Summary
@effect/opentelemetry's MetricProducer does not query the registered MetricReader's selectAggregationTemporality(instrumentType) — it always hardcodes AggregationTemporality.CUMULATIVE on every Sum data point (Counter and UpDownCounter). This means any non-default temporality preference (e.g. temporalityPreference: DELTA on OTLPMetricExporter) is silently ineffective: temporality is baked into the data point by the producer before the exporter sees it.
Per the OTel spec, producers should query the reader for the preferred temporality per instrument type rather than choosing unconditionally.
Note: an earlier version of this issue claimed this causes Datadog to display inflated rates. That turned out to be a different bug on our side (we were missing { incremental: true } on Metric.counter, so the metric was exported as a non-monotonic Sum and DD treated it as a gauge). With incremental: true, DD ingests the cumulative monotonic Sum correctly via internal cumulative→delta conversion. The temporality-hardcode issue described here is real and reproducible, but its impact is narrower than I originally framed it. Apologies for the noise.
Reproduction
import { Effect, Metric } from "effect";
import { Metrics as EffectOtel, Resource } from "@effect/opentelemetry";
import {
AggregationTemporality,
InMemoryMetricExporter,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
const exporter = new InMemoryMetricExporter(AggregationTemporality.DELTA);
const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 60_000_000 });
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const counter = Metric.counter("test.counter", { incremental: true });
yield* Effect.succeed(5).pipe(Metric.increment(counter));
yield* Effect.succeed(7).pipe(Metric.increment(counter));
yield* Effect.promise(() => reader.forceFlush());
}).pipe(
Effect.provide(EffectOtel.layer(() => reader)),
Effect.provide(Resource.layer({ serviceName: "test" }))
)
)
);
const data = exporter.getMetrics();
const counter = data[0].scopeMetrics[0].metrics[0];
console.log(counter.aggregationTemporality); // 1 (CUMULATIVE) — expected 0 (DELTA)
console.log(counter.isMonotonic); // true (good)
console.log(counter.dataPoints[0].value); // 12 (good)
The InMemoryMetricExporter is constructed with AggregationTemporality.DELTA, but the produced data point arrives as CUMULATIVE. Same result with a real OTLPMetricExporter({ temporalityPreference: DELTA }).
Root cause
packages/opentelemetry/src/internal/metrics.ts (line numbers from v0.61.0):
if (MetricState.isCounterState(metricState)) {
return {
descriptor: makeDescriptor(metricPair),
dataPointType: SdkMetrics.DataPointType.SUM,
isMonotonic: descriptor.type === SdkMetrics.InstrumentType.COUNTER,
aggregationTemporality: SdkMetrics.AggregationTemporality.CUMULATIVE, // <-- hardcoded
dataPoints: [{ /* ... */ }],
};
}
The same hardcoded value appears for every Sum-typed path in the file (Counter, UpDownCounter, FrequencyMetric, etc.). No producer-side or layer-level option to override it.
Expected behavior
The producer should look up the reader's preferred temporality for the instrument type and stamp that on each data point — e.g. by calling reader.selectAggregationTemporality(instrumentType) and using the returned value. This is the spec-compliant approach.
Impact
Narrow but real. Affects pipelines where the configured backend requires (or strongly prefers) DELTA temporality and where the user expects temporalityPreference on the exporter to control that:
- Some OTel collector configurations downstream of
@effect/opentelemetry that are themselves set to DELTA-only routing.
- Backends that do not handle cumulative monotonic Sums on ingest (most major vendors do, so this is genuinely niche).
It does not noticeably affect default OTLP-to-Datadog or Prometheus-pull setups, which handle cumulative monotonic Sums correctly.
Workaround
For users who genuinely need DELTA-temporality export and can't change the backend:
- Bypass
@effect/opentelemetry for the affected instruments — e.g. emit via a different SDK (dd-trace's dogstatsd, raw OTel API) and keep @effect/opentelemetry for everything else.
- There's no in-library escape hatch today.
Suggested fix
Either:
- Drive temporality from the registered MetricReader — query
reader.selectAggregationTemporality(instrumentType) per produced data point and stamp accordingly. Spec-compliant; preserves all existing default behavior for users on CUMULATIVE-preferring readers.
- Accept a temporality option on
EffectOtel.layer / makeProducer — escape hatch: { counterTemporality: DELTA, upDownCounterTemporality: ... }. Simpler, less correct.
Happy to PR (1) if there's interest.
Environment
@effect/opentelemetry 0.61.0
@opentelemetry/sdk-metrics 2.2.0
@opentelemetry/exporter-metrics-otlp-grpc 0.207.0
Summary
@effect/opentelemetry'sMetricProducerdoes not query the registeredMetricReader'sselectAggregationTemporality(instrumentType)— it always hardcodesAggregationTemporality.CUMULATIVEon every Sum data point (Counter and UpDownCounter). This means any non-default temporality preference (e.g.temporalityPreference: DELTAonOTLPMetricExporter) is silently ineffective: temporality is baked into the data point by the producer before the exporter sees it.Per the OTel spec, producers should query the reader for the preferred temporality per instrument type rather than choosing unconditionally.
Reproduction
The
InMemoryMetricExporteris constructed withAggregationTemporality.DELTA, but the produced data point arrives as CUMULATIVE. Same result with a realOTLPMetricExporter({ temporalityPreference: DELTA }).Root cause
packages/opentelemetry/src/internal/metrics.ts(line numbers from v0.61.0):The same hardcoded value appears for every Sum-typed path in the file (Counter, UpDownCounter, FrequencyMetric, etc.). No producer-side or layer-level option to override it.
Expected behavior
The producer should look up the reader's preferred temporality for the instrument type and stamp that on each data point — e.g. by calling
reader.selectAggregationTemporality(instrumentType)and using the returned value. This is the spec-compliant approach.Impact
Narrow but real. Affects pipelines where the configured backend requires (or strongly prefers) DELTA temporality and where the user expects
temporalityPreferenceon the exporter to control that:@effect/opentelemetrythat are themselves set to DELTA-only routing.It does not noticeably affect default OTLP-to-Datadog or Prometheus-pull setups, which handle cumulative monotonic Sums correctly.
Workaround
For users who genuinely need DELTA-temporality export and can't change the backend:
@effect/opentelemetryfor the affected instruments — e.g. emit via a different SDK (dd-trace's dogstatsd, raw OTel API) and keep@effect/opentelemetryfor everything else.Suggested fix
Either:
reader.selectAggregationTemporality(instrumentType)per produced data point and stamp accordingly. Spec-compliant; preserves all existing default behavior for users on CUMULATIVE-preferring readers.EffectOtel.layer/makeProducer— escape hatch:{ counterTemporality: DELTA, upDownCounterTemporality: ... }. Simpler, less correct.Happy to PR (1) if there's interest.
Environment
@effect/opentelemetry0.61.0@opentelemetry/sdk-metrics2.2.0@opentelemetry/exporter-metrics-otlp-grpc0.207.0