| 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 | */ |
| 13 | import type { MetricProducer, MetricReader } from "@opentelemetry/sdk-metrics" |
| 14 | import type * as Arr from "effect/Array" |
| 15 | import type * as Duration from "effect/Duration" |
| 16 | import * as Effect from "effect/Effect" |
| 17 | import type { LazyArg } from "effect/Function" |
| 18 | import * as Layer from "effect/Layer" |
| 19 | import type * as Scope from "effect/Scope" |
| 20 | import { MetricProducerImpl } from "./internal/metrics.ts" |
| 21 | import { 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 | */ |
| 38 | export 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 | */ |
| 61 | export 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 | */ |
| 75 | export 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 | */ |
| 144 | export 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 | )) |