Implement planned topic: 0023-buffered-metrics

Adds TypeScript reference for the MetricMeter API and MetricsBuffer,
covering all four instrument types (Counter, UpDownCounter, Gauge,
Histogram), how to access the meter from worker/workflow/activity
contexts, and how to drain BufferedMetricUpdate events for custom
export. Grounded against the TS SDK typedoc since the local docs
clone is silent on these APIs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
skill-sync[bot]
2026-05-14 18:48:21 +00:00
parent 3c8ed616bd
commit da5f595271
2 changed files with 225 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
# TypeScript SDK Buffered Metrics & Custom Metrics
> [!NOTE]
> The Metric API and buffered metrics are experimental in the TypeScript SDK; the APIs may change. <!-- typedoc: common.MetricMeter (description), worker.MetricsBuffer (description) -->
This reference covers two related TypeScript SDK features:
1. **The `MetricMeter` API** — emit custom metrics from worker, workflow, and activity code using four instrument types.
2. **`MetricsBuffer`** — in-process buffer for capturing all metric updates (Core + custom) when you want to forward them somewhere other than Prometheus or OTLP (e.g., StatsD, Datadog DogStatsD, in-process aggregation).
For the standard Prometheus / OTel collector export paths, see `references/typescript/observability.md`.
## When to use which
| Goal | Use |
|---|---|
| Scrape metrics with Prometheus | `telemetryOptions.metrics.prometheus.bindAddress` (see `observability.md`) |
| Push to an OTLP collector | `telemetryOptions.metrics.otel.url` (see `observability.md`) |
| Custom export (StatsD, Datadog client, log shipper, tests) | `MetricsBuffer` + `retrieveUpdates()` |
| Emit your own application metrics | `metricMeter` from the relevant package |
`MetricsBuffer` and a Prometheus / OTel exporter are **mutually exclusive** on the `metrics` field — pick one transport per Runtime.
## Instrument types
The `MetricMeter` interface exposes four instrument types: <!-- typedoc: common.MetricMeter -->
| Method | Returns | `kind` literal | Use for |
|---|---|---|---|
| `createCounter(name, unit?, description?)` | `MetricCounter` | `"counter"` | Monotonically increasing totals (events, requests). <!-- typedoc: common.MetricCounter --> |
| `createUpDownCounter(name, unit?, description?)` | `MetricUpDownCounter` | `"up-down-counter"` | Values that go up and down (in-flight requests, queue depth, active connections). <!-- typedoc: common.MetricUpDownCounter --> |
| `createGauge(name, valueType?, unit?, description?)` | `MetricGauge` | `"gauge"` | Instantaneous measurements set to an absolute value. <!-- typedoc: common.MetricGauge --> |
| `createHistogram(name, valueType?, unit?, description?)` | `MetricHistogram` | `"histogram"` | Distributions of non-negative values. <!-- typedoc: common.MetricHistogram --> |
`MetricCounter` and `MetricUpDownCounter` always record integers (`valueType: "int"`). Only `createGauge` and `createHistogram` accept a `valueType` parameter (`"int" | "float"`). <!-- typedoc: common.MetricCounter, common.MetricUpDownCounter, common.NumericMetricValueType -->
`MetricKind` is `"counter" | "histogram" | "gauge" | "up-down-counter"` — note the hyphenated form. <!-- typedoc: common.MetricKind -->
### Instrument methods
```typescript
counter.add(value: number, extraTags?: MetricTags): void // value ≥ 0
upDownCounter.add(value: number, extraTags?: MetricTags): void // value may be negative
gauge.set(value: number, extraTags?: MetricTags): void
histogram.record(value: number, extraTags?: MetricTags): void // value ≥ 0
```
<!-- typedoc: common.MetricCounter#add, common.MetricUpDownCounter#add, common.MetricGauge#set, common.MetricHistogram#record -->
Every instrument also has `withTags(tags: MetricTags): <SameInstrument>` which returns a clone with permanent extra tags. `MetricTags` is `Record<string, string | number | boolean>`. <!-- typedoc: common.MetricMeter#withTags, common.MetricTags -->
## Accessing the meter
`metricMeter` is the entry point in each runtime context. It is a **property, not a function**.
**Worker / Client process (top-level):** <!-- typedoc: worker.Runtime#metricMeter -->
```typescript
import { Runtime } from '@temporalio/worker';
const meter = Runtime.instance().metricMeter;
const tasksInFlight = meter.createUpDownCounter('app_tasks_in_flight', undefined, 'Tasks currently being processed');
tasksInFlight.add(1, { worker: 'payments' });
// ...later
tasksInFlight.add(-1, { worker: 'payments' });
```
**Inside a Workflow:** <!-- typedoc: workflow namespace#metricMeter -->
```typescript
import { metricMeter } from '@temporalio/workflow';
export async function chargeWorkflow(orderId: string): Promise<void> {
const charges = metricMeter.createCounter('charges_started');
charges.add(1, { orderId });
}
```
The workflow `metricMeter` is automatically tagged with workflow context.
**Inside an Activity:** <!-- typedoc: activity namespace#metricMeter -->
```typescript
import { metricMeter } from '@temporalio/activity';
export async function callPaymentGateway(orderId: string): Promise<void> {
const latency = metricMeter.createHistogram('gateway_latency', 'float', 'ms');
const start = performance.now();
// ... do work ...
latency.record(performance.now() - start, { orderId });
}
```
The activity `metricMeter` is automatically tagged with activity context; `ActivityOutboundCallsInterceptor.getMetricTags()` can add custom tags. <!-- typedoc: activity namespace#metricMeter -->
If telemetry is not configured, the meter resolves to `noopMetricMeter` — calls are silently dropped. <!-- typedoc: common namespace#noopMetricMeter -->
## Buffered metrics
`MetricsBuffer` (exported from `@temporalio/worker`) captures every metric update — both Core-emitted SDK metrics and anything you record through `metricMeter` — into an in-memory queue you drain on your schedule. <!-- typedoc: worker namespace -->
### Setup
```typescript
import { MetricsBuffer, Runtime } from '@temporalio/worker';
const buffer = new MetricsBuffer({ maxBufferSize: 100_000 });
Runtime.install({
telemetryOptions: {
metrics: buffer,
},
});
```
<!-- typedoc: worker.MetricsBuffer, worker.MetricsBufferOptions -->
`MetricsBufferOptions`: <!-- typedoc: worker.MetricsBufferOptions -->
| Option | Type | Default | Notes |
|---|---|---|---|
| `maxBufferSize` | `number` | `10000` | Max events buffered before new updates are dropped and an error is logged. |
| `useSecondsForDurations` | `boolean` | `false` | If `true`, duration metrics use seconds instead of milliseconds. |
### Draining
```typescript
const runtime = Runtime.instance();
const buffer = runtime.metricsBuffer;
if (!buffer) return; // buffered metrics not configured
setInterval(() => {
for (const update of buffer.retrieveUpdates()) {
forward(update);
}
}, 1_000);
```
<!-- typedoc: worker.Runtime#metricsBuffer, worker.MetricsBuffer#retrieveUpdates -->
`retrieveUpdates()` returns an `ArrayIterator<BufferedMetricUpdate>` containing every event accumulated since the last call.
### `BufferedMetricUpdate` shape
```typescript
interface BufferedMetricUpdate {
attributes: MetricTags; // tags for this update
metric: Metric; // the metric (includes kind, name, valueType, unit?, description?)
value: number; // delta for counters/up-down-counters; absolute for gauges; sample for histograms
}
```
<!-- typedoc: worker.BufferedMetricUpdate -->
The SDK reuses `attributes` and `metric` objects across updates for performance, so do not store references — copy what you need before the next call to `retrieveUpdates()`. <!-- typedoc: worker.BufferedMetricUpdate -->
Dispatch by `metric.kind`:
```typescript
switch (update.metric.kind) {
case 'counter': return statsd.increment(update.metric.name, update.value, update.attributes);
case 'up-down-counter': return statsd.gauge(update.metric.name, update.value, update.attributes); // delta
case 'gauge': return statsd.gauge(update.metric.name, update.value, update.attributes);
case 'histogram': return statsd.distribution(update.metric.name, update.value, update.attributes);
}
```
## Hard constraints
- **Drain on a timer.** If `retrieveUpdates()` is not called regularly, the buffer fills, new updates are dropped, and an error is logged. Size the buffer for your drain interval. <!-- typedoc: worker.MetricsBuffer#retrieveUpdates, worker.MetricsBufferOptions#maxBufferSize -->
- **Buffered metrics and Prometheus / OTel are exclusive.** The `metrics` field on `telemetryOptions` is a single transport — install either a `MetricsBuffer` or a `PrometheusMetricsExporter` / `OtelCollectorExporter`, not both. <!-- typedoc: worker.RuntimeOptions, worker.PrometheusMetricsExporter, worker.OtelCollectorExporter -->
- **`Runtime.install` is once per process.** Configure it before constructing any `Worker` or `Client`. <!-- typedoc: worker.Runtime#install -->
- **Do not retain `attributes`/`metric` references across iterations.** The SDK mutates the same objects between events. Copy fields you need to keep. <!-- typedoc: worker.BufferedMetricUpdate -->
- **Counters and up-down-counters are int-only.** Pass integer values to `add`; floats are truncated to the declared `valueType`. <!-- typedoc: common.MetricCounter, common.MetricUpDownCounter -->
- **`metricMeter` is a property.** Access via `Runtime.instance().metricMeter` or `import { metricMeter } from '@temporalio/workflow' | '@temporalio/activity'`. There is no `RuntimeMetricMeter` exported type. <!-- typedoc: worker.Runtime#metricMeter, workflow namespace#metricMeter, activity namespace#metricMeter -->
## Common mistakes
| Mistake | Fix |
|---|---|
| `meter.createUpDownCounter(name, 'int', ...)` | Counters/up-down-counters take no `valueType` — only `(name, unit?, description?)`. <!-- typedoc: common.MetricMeter#createUpDownCounter --> |
| Using `"upDownCounter"` as the kind literal | The literal is `"up-down-counter"`. <!-- typedoc: common.MetricKind --> |
| `Runtime.instance().retrieveBufferedMetrics()` | `Runtime.instance().metricsBuffer?.retrieveUpdates()`. <!-- typedoc: worker.Runtime, worker.MetricsBuffer#retrieveUpdates --> |
| `import { MetricsBuffer } from '@temporalio/common'` | Import from `@temporalio/worker`. <!-- typedoc: worker namespace --> |
| Calling `meter.createCounter()` without `Runtime.install` | Without telemetry configured, `metricMeter` is `noopMetricMeter` and all updates are dropped silently. <!-- typedoc: common namespace#noopMetricMeter --> |
| Storing `update.attributes` from a previous iteration | Objects are reused — copy before the next `retrieveUpdates` call. <!-- typedoc: worker.BufferedMetricUpdate --> |
| Configuring both `prometheus` and a `MetricsBuffer` | One transport per `telemetryOptions.metrics`. <!-- typedoc: worker.RuntimeOptions --> |
## Worked example: forward to StatsD with an UpDownCounter
```typescript
import { MetricsBuffer, Runtime } from '@temporalio/worker';
import { StatsD } from 'hot-shots';
const buffer = new MetricsBuffer({ maxBufferSize: 50_000 });
Runtime.install({ telemetryOptions: { metrics: buffer } });
const statsd = new StatsD();
setInterval(() => {
for (const { metric, value, attributes } of buffer.retrieveUpdates()) {
const tags = Object.entries(attributes).map(([k, v]) => `${k}:${v}`);
switch (metric.kind) {
case 'counter':
case 'up-down-counter':
statsd.count(metric.name, value, tags);
break;
case 'gauge':
statsd.gauge(metric.name, value, tags);
break;
case 'histogram':
statsd.distribution(metric.name, value, tags);
break;
}
}
}, 1_000);
// Application metric using the runtime meter:
const meter = Runtime.instance().metricMeter;
const inFlight = meter.createUpDownCounter('app_orders_in_flight');
inFlight.add(1, { region: 'us-east' });
// ...
inFlight.add(-1, { region: 'us-east' });
```
<!-- typedoc: worker.MetricsBuffer, worker.Runtime#metricMeter, common.MetricMeter#createUpDownCounter, common.MetricUpDownCounter#add -->
+4
View File
@@ -100,6 +100,10 @@ Runtime.install({
});
```
### Custom metrics & buffered metrics
For emitting application metrics with the `MetricMeter` API (Counter, UpDownCounter, Gauge, Histogram) or forwarding all metric updates to a custom transport via `MetricsBuffer`, see `references/typescript/buffered-metrics.md`.
## Search Attributes (Visibility)
See the Search Attributes section of `references/typescript/data-handling.md`