From da5f595271f70b389b9829ebfee30f98b6e5bdba Mon Sep 17 00:00:00 2001 From: "skill-sync[bot]" Date: Thu, 14 May 2026 18:48:21 +0000 Subject: [PATCH 1/2] 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 --- references/typescript/buffered-metrics.md | 221 ++++++++++++++++++++++ references/typescript/observability.md | 4 + 2 files changed, 225 insertions(+) create mode 100644 references/typescript/buffered-metrics.md diff --git a/references/typescript/buffered-metrics.md b/references/typescript/buffered-metrics.md new file mode 100644 index 0000000..5281360 --- /dev/null +++ b/references/typescript/buffered-metrics.md @@ -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. + +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: + +| Method | Returns | `kind` literal | Use for | +|---|---|---|---| +| `createCounter(name, unit?, description?)` | `MetricCounter` | `"counter"` | Monotonically increasing totals (events, requests). | +| `createUpDownCounter(name, unit?, description?)` | `MetricUpDownCounter` | `"up-down-counter"` | Values that go up and down (in-flight requests, queue depth, active connections). | +| `createGauge(name, valueType?, unit?, description?)` | `MetricGauge` | `"gauge"` | Instantaneous measurements set to an absolute value. | +| `createHistogram(name, valueType?, unit?, description?)` | `MetricHistogram` | `"histogram"` | Distributions of non-negative values. | + +`MetricCounter` and `MetricUpDownCounter` always record integers (`valueType: "int"`). Only `createGauge` and `createHistogram` accept a `valueType` parameter (`"int" | "float"`). + +`MetricKind` is `"counter" | "histogram" | "gauge" | "up-down-counter"` — note the hyphenated form. + +### 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 +``` + + +Every instrument also has `withTags(tags: MetricTags): ` which returns a clone with permanent extra tags. `MetricTags` is `Record`. + +## Accessing the meter + +`metricMeter` is the entry point in each runtime context. It is a **property, not a function**. + +**Worker / Client process (top-level):** + +```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:** + +```typescript +import { metricMeter } from '@temporalio/workflow'; + +export async function chargeWorkflow(orderId: string): Promise { + const charges = metricMeter.createCounter('charges_started'); + charges.add(1, { orderId }); +} +``` + +The workflow `metricMeter` is automatically tagged with workflow context. + +**Inside an Activity:** + +```typescript +import { metricMeter } from '@temporalio/activity'; + +export async function callPaymentGateway(orderId: string): Promise { + 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. + +If telemetry is not configured, the meter resolves to `noopMetricMeter` — calls are silently dropped. + +## 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. + +### Setup + +```typescript +import { MetricsBuffer, Runtime } from '@temporalio/worker'; + +const buffer = new MetricsBuffer({ maxBufferSize: 100_000 }); + +Runtime.install({ + telemetryOptions: { + metrics: buffer, + }, +}); +``` + + +`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); +``` + + +`retrieveUpdates()` returns an `ArrayIterator` 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 +} +``` + + +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()`. + +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. +- **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. +- **`Runtime.install` is once per process.** Configure it before constructing any `Worker` or `Client`. +- **Do not retain `attributes`/`metric` references across iterations.** The SDK mutates the same objects between events. Copy fields you need to keep. +- **Counters and up-down-counters are int-only.** Pass integer values to `add`; floats are truncated to the declared `valueType`. +- **`metricMeter` is a property.** Access via `Runtime.instance().metricMeter` or `import { metricMeter } from '@temporalio/workflow' | '@temporalio/activity'`. There is no `RuntimeMetricMeter` exported type. + +## Common mistakes + +| Mistake | Fix | +|---|---| +| `meter.createUpDownCounter(name, 'int', ...)` | Counters/up-down-counters take no `valueType` — only `(name, unit?, description?)`. | +| Using `"upDownCounter"` as the kind literal | The literal is `"up-down-counter"`. | +| `Runtime.instance().retrieveBufferedMetrics()` | `Runtime.instance().metricsBuffer?.retrieveUpdates()`. | +| `import { MetricsBuffer } from '@temporalio/common'` | Import from `@temporalio/worker`. | +| Calling `meter.createCounter()` without `Runtime.install` | Without telemetry configured, `metricMeter` is `noopMetricMeter` and all updates are dropped silently. | +| Storing `update.attributes` from a previous iteration | Objects are reused — copy before the next `retrieveUpdates` call. | +| Configuring both `prometheus` and a `MetricsBuffer` | One transport per `telemetryOptions.metrics`. | + +## 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' }); +``` + diff --git a/references/typescript/observability.md b/references/typescript/observability.md index 211fbc6..22f53f9 100644 --- a/references/typescript/observability.md +++ b/references/typescript/observability.md @@ -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` From b8543897fcd0188771e514bafc3ae4de3b51ada4 Mon Sep 17 00:00:00 2001 From: "skill-sync[bot]" Date: Thu, 14 May 2026 19:03:36 +0000 Subject: [PATCH 2/2] Finalize draft for 0023-buffered-metrics --- references/java/integrations/spring-ai.md | 1 - references/typescript/buffered-metrics.md | 66 +++++++++++------------ 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/references/java/integrations/spring-ai.md b/references/java/integrations/spring-ai.md index 5ee0704..ae5154f 100644 --- a/references/java/integrations/spring-ai.md +++ b/references/java/integrations/spring-ai.md @@ -217,7 +217,6 @@ Media image = new Media(MimeTypeUtils.IMAGE_PNG, URI.create("https://cdn.example For anything larger than a small thumbnail, route the bytes to a binary store from an Activity and pass only the URL across the conversation. - ## Vector stores, embeddings, and MCP When the corresponding Spring AI modules (`spring-ai-rag`, `spring-ai-mcp`) are on the classpath, the integration registers Activities for vector stores, embeddings, and MCP tool calls automatically. Inject the matching Spring AI types into your Activities or Workflows and use them as you would in any Spring AI application — each operation executes through a Temporal Activity. diff --git a/references/typescript/buffered-metrics.md b/references/typescript/buffered-metrics.md index 5281360..1331263 100644 --- a/references/typescript/buffered-metrics.md +++ b/references/typescript/buffered-metrics.md @@ -1,7 +1,7 @@ # TypeScript SDK Buffered Metrics & Custom Metrics > [!NOTE] -> The Metric API and buffered metrics are experimental in the TypeScript SDK; the APIs may change. +> The Metric API and buffered metrics are experimental in the TypeScript SDK; the APIs may change. This reference covers two related TypeScript SDK features: @@ -23,18 +23,18 @@ For the standard Prometheus / OTel collector export paths, see `references/types ## Instrument types -The `MetricMeter` interface exposes four instrument types: +The `MetricMeter` interface exposes four instrument types: | Method | Returns | `kind` literal | Use for | |---|---|---|---| -| `createCounter(name, unit?, description?)` | `MetricCounter` | `"counter"` | Monotonically increasing totals (events, requests). | -| `createUpDownCounter(name, unit?, description?)` | `MetricUpDownCounter` | `"up-down-counter"` | Values that go up and down (in-flight requests, queue depth, active connections). | -| `createGauge(name, valueType?, unit?, description?)` | `MetricGauge` | `"gauge"` | Instantaneous measurements set to an absolute value. | -| `createHistogram(name, valueType?, unit?, description?)` | `MetricHistogram` | `"histogram"` | Distributions of non-negative values. | +| `createCounter(name, unit?, description?)` | `MetricCounter` | `"counter"` | Monotonically increasing totals (events, requests). | +| `createUpDownCounter(name, unit?, description?)` | `MetricUpDownCounter` | `"up-down-counter"` | Values that go up and down (in-flight requests, queue depth, active connections). | +| `createGauge(name, valueType?, unit?, description?)` | `MetricGauge` | `"gauge"` | Instantaneous measurements set to an absolute value. | +| `createHistogram(name, valueType?, unit?, description?)` | `MetricHistogram` | `"histogram"` | Distributions of non-negative values. | -`MetricCounter` and `MetricUpDownCounter` always record integers (`valueType: "int"`). Only `createGauge` and `createHistogram` accept a `valueType` parameter (`"int" | "float"`). +`MetricCounter` and `MetricUpDownCounter` always record integers (`valueType: "int"`). Only `createGauge` and `createHistogram` accept a `valueType` parameter (`"int" | "float"`). -`MetricKind` is `"counter" | "histogram" | "gauge" | "up-down-counter"` — note the hyphenated form. +`MetricKind` is `"counter" | "histogram" | "gauge" | "up-down-counter"` — note the hyphenated form. ### Instrument methods @@ -44,15 +44,14 @@ upDownCounter.add(value: number, extraTags?: MetricTags): void // value may be gauge.set(value: number, extraTags?: MetricTags): void histogram.record(value: number, extraTags?: MetricTags): void // value ≥ 0 ``` - -Every instrument also has `withTags(tags: MetricTags): ` which returns a clone with permanent extra tags. `MetricTags` is `Record`. +Every instrument also has `withTags(tags: MetricTags): ` which returns a clone with permanent extra tags. `MetricTags` is `Record`. ## Accessing the meter `metricMeter` is the entry point in each runtime context. It is a **property, not a function**. -**Worker / Client process (top-level):** +**Worker / Client process (top-level):** ```typescript import { Runtime } from '@temporalio/worker'; @@ -64,7 +63,7 @@ tasksInFlight.add(1, { worker: 'payments' }); tasksInFlight.add(-1, { worker: 'payments' }); ``` -**Inside a Workflow:** +**Inside a Workflow:** ```typescript import { metricMeter } from '@temporalio/workflow'; @@ -77,7 +76,7 @@ export async function chargeWorkflow(orderId: string): Promise { The workflow `metricMeter` is automatically tagged with workflow context. -**Inside an Activity:** +**Inside an Activity:** ```typescript import { metricMeter } from '@temporalio/activity'; @@ -90,13 +89,13 @@ export async function callPaymentGateway(orderId: string): Promise { } ``` -The activity `metricMeter` is automatically tagged with activity context; `ActivityOutboundCallsInterceptor.getMetricTags()` can add custom tags. +The activity `metricMeter` is automatically tagged with activity context; `ActivityOutboundCallsInterceptor.getMetricTags()` can add custom tags. -If telemetry is not configured, the meter resolves to `noopMetricMeter` — calls are silently dropped. +If telemetry is not configured, the meter resolves to `noopMetricMeter` — calls are silently dropped. ## 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. +`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. ### Setup @@ -111,9 +110,8 @@ Runtime.install({ }, }); ``` - -`MetricsBufferOptions`: +`MetricsBufferOptions`: | Option | Type | Default | Notes | |---|---|---|---| @@ -133,7 +131,6 @@ setInterval(() => { } }, 1_000); ``` - `retrieveUpdates()` returns an `ArrayIterator` containing every event accumulated since the last call. @@ -146,9 +143,8 @@ interface BufferedMetricUpdate { value: number; // delta for counters/up-down-counters; absolute for gauges; sample for histograms } ``` - -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()`. +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()`. Dispatch by `metric.kind`: @@ -163,24 +159,24 @@ switch (update.metric.kind) { ## 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. -- **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. -- **`Runtime.install` is once per process.** Configure it before constructing any `Worker` or `Client`. -- **Do not retain `attributes`/`metric` references across iterations.** The SDK mutates the same objects between events. Copy fields you need to keep. -- **Counters and up-down-counters are int-only.** Pass integer values to `add`; floats are truncated to the declared `valueType`. -- **`metricMeter` is a property.** Access via `Runtime.instance().metricMeter` or `import { metricMeter } from '@temporalio/workflow' | '@temporalio/activity'`. There is no `RuntimeMetricMeter` exported type. +- **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. +- **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. +- **`Runtime.install` is once per process.** Configure it before constructing any `Worker` or `Client`. +- **Do not retain `attributes`/`metric` references across iterations.** The SDK mutates the same objects between events. Copy fields you need to keep. +- **Counters and up-down-counters are int-only.** Pass integer values to `add`; floats are truncated to the declared `valueType`. +- **`metricMeter` is a property.** Access via `Runtime.instance().metricMeter` or `import { metricMeter } from '@temporalio/workflow' | '@temporalio/activity'`. There is no `RuntimeMetricMeter` exported type. ## Common mistakes | Mistake | Fix | |---|---| -| `meter.createUpDownCounter(name, 'int', ...)` | Counters/up-down-counters take no `valueType` — only `(name, unit?, description?)`. | -| Using `"upDownCounter"` as the kind literal | The literal is `"up-down-counter"`. | -| `Runtime.instance().retrieveBufferedMetrics()` | `Runtime.instance().metricsBuffer?.retrieveUpdates()`. | -| `import { MetricsBuffer } from '@temporalio/common'` | Import from `@temporalio/worker`. | -| Calling `meter.createCounter()` without `Runtime.install` | Without telemetry configured, `metricMeter` is `noopMetricMeter` and all updates are dropped silently. | -| Storing `update.attributes` from a previous iteration | Objects are reused — copy before the next `retrieveUpdates` call. | -| Configuring both `prometheus` and a `MetricsBuffer` | One transport per `telemetryOptions.metrics`. | +| `meter.createUpDownCounter(name, 'int', ...)` | Counters/up-down-counters take no `valueType` — only `(name, unit?, description?)`. | +| Using `"upDownCounter"` as the kind literal | The literal is `"up-down-counter"`. | +| `Runtime.instance().retrieveBufferedMetrics()` | `Runtime.instance().metricsBuffer?.retrieveUpdates()`. | +| `import { MetricsBuffer } from '@temporalio/common'` | Import from `@temporalio/worker`. | +| Calling `meter.createCounter()` without `Runtime.install` | Without telemetry configured, `metricMeter` is `noopMetricMeter` and all updates are dropped silently. | +| Storing `update.attributes` from a previous iteration | Objects are reused — copy before the next `retrieveUpdates` call. | +| Configuring both `prometheus` and a `MetricsBuffer` | One transport per `telemetryOptions.metrics`. | ## Worked example: forward to StatsD with an UpDownCounter @@ -218,4 +214,4 @@ inFlight.add(1, { region: 'us-east' }); // ... inFlight.add(-1, { region: 'us-east' }); ``` - +