1/**
2 * OpenTelemetry support for Effect metrics.
3 *
4 * This module exposes Effect metrics as an OpenTelemetry `MetricProducer`.
5 * `makeProducer` creates the producer, `registerProducer` attaches it to one
6 * or more SDK `MetricReader`s, and `layer` manages that setup in a scoped
7 * layer. The `TemporalityPreference` type lets callers choose cumulative or
8 * delta metric values.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import type { MetricProducer, MetricReader } from "@opentelemetry/sdk-metrics"
14import type * as Arr from "effect/Array"
15import type * as Duration from "effect/Duration"
16import * as Effect from "effect/Effect"
17import type { LazyArg } from "effect/Function"
18import * as Layer from "effect/Layer"
19import type * as Scope from "effect/Scope"
20import { MetricProducerImpl } from "./internal/metrics.ts"
21import { Resource } from "./Resource.ts"
22
23/**
24 * Determines how metric values relate to the time interval over which they
25 * are aggregated.
26 *
27 * **Details**
28 *
29 * `cumulative` reports total since a fixed start time. Each data point depends
30 * on all previous measurements. This is the default behavior. `delta` reports
31 * changes since the last export. Each interval is independent with no
32 * dependency on previous measurements.
33 *
34 * @stability unstable
35 * @category models
36 * @since 4.0.0
37 */
38export type TemporalityPreference = "cumulative" | "delta"
39
40/**
41 * Creates an OpenTelemetry metric producer from Effect metrics.
42 *
43 * **When to use**
44 *
45 * Use when you need a `MetricProducer` for manually wiring Effect metrics into
46 * OpenTelemetry instead of using the scoped `layer` helper.
47 *
48 * **Details**
49 *
50 * Requires the current OpenTelemetry `Resource`, captures the current Effect
51 * context, and uses cumulative temporality by default. Pass `"delta"` for
52 * interval-based values.
53 *
54 * @see {@link registerProducer} for attaching a producer to metric readers
55 * @see {@link layer} for creating and registering a producer in a scoped layer
56 *
57 * @stability unstable
58 * @category constructors
59 * @since 4.0.0
60 */
61export const makeProducer = (temporality?: TemporalityPreference): Effect.Effect<MetricProducer, never, Resource> =>
62 Effect.gen(function*() {
63 const resource = yield* Resource
64 const services = yield* Effect.context<never>()
65 return new MetricProducerImpl(resource, services, temporality)
66 })
67
68/**
69 * Registers a metric producer with one or more metric readers.
70 *
71 * @stability unstable
72 * @category resource management
73 * @since 4.0.0
74 */
75export const registerProducer = (
76 self: MetricProducer,
77 metricReader: LazyArg<MetricReader | Arr.NonEmptyReadonlyArray<MetricReader>>,
78 options?: {
79 readonly shutdownTimeout?: Duration.Input | undefined
80 }
81): Effect.Effect<Array<any>, never, Scope.Scope> =>
82 Effect.acquireRelease(
83 Effect.sync(() => {
84 const reader = metricReader()
85 const readers: Array<MetricReader> = Array.isArray(reader) ? reader : [reader] as any
86 readers.forEach((reader) => reader.setMetricProducer(self instanceof MetricProducerImpl ? self.fork() : self))
87 return readers
88 }),
89 (readers) =>
90 Effect.promise(() =>
91 Promise.all(
92 readers.map((reader) => reader.shutdown())
93 )
94 ).pipe(
95 Effect.ignore,
96 Effect.interruptible,
97 Effect.timeoutOption(options?.shutdownTimeout ?? 3000)
98 )
99 )
100
101/**
102 * Creates a Layer that registers a metric producer with metric readers.
103 *
104 * **Example** (Exporting delta metrics)
105 *
106 * ```ts import.meta.vitest
107 * import { OtelMetrics, Resource } from "@effect/opentelemetry"
108 * import {
109 * AggregationTemporality,
110 * InMemoryMetricExporter,
111 * PeriodicExportingMetricReader
112 * } from "@opentelemetry/sdk-metrics"
113 * import { Effect, Layer, Metric } from "effect"
114 *
115 * const exporter = new InMemoryMetricExporter(AggregationTemporality.DELTA)
116 * const reader = new PeriodicExportingMetricReader({
117 * exporter,
118 * exportIntervalMillis: 60_000
119 * })
120 * const metricsLayer = OtelMetrics.layer(() => reader, { temporality: "delta" }).pipe(
121 * Layer.provide(Resource.layerEmpty)
122 * )
123 *
124 * const program = Effect.gen(function*() {
125 * yield* Metric.update(Metric.counter("docs.requests", { incremental: true }), 2)
126 * yield* Effect.promise(() => reader.forceFlush())
127 *
128 * const metric = exporter.getMetrics()[0]?.scopeMetrics[0]?.metrics.find(
129 * (metric) => metric.descriptor.name === "docs.requests"
130 * )
131 * return [metric?.descriptor.name, metric?.aggregationTemporality, metric?.dataPoints[0]?.value] as const
132 * }).pipe(
133 * Effect.provide(metricsLayer),
134 * Effect.provideService(Metric.MetricRegistry, new Map())
135 * )
136 *
137 * await Effect.runPromise(program) // => ["docs.requests", AggregationTemporality.DELTA, 2]
138 * ```
139 *
140 * @stability unstable
141 * @category layers
142 * @since 4.0.0
143 */
144export const layer = (
145 evaluate: LazyArg<MetricReader | Arr.NonEmptyReadonlyArray<MetricReader>>,
146 options?: {
147 readonly shutdownTimeout?: Duration.Input | undefined
148 readonly temporality?: TemporalityPreference | undefined
149 }
150): Layer.Layer<never, never, Resource> =>
151 Layer.effectDiscard(Effect.flatMap(
152 makeProducer(options?.temporality),
153 (producer) => registerProducer(producer, evaluate, options)
154 ))