1/**
2 * Node.js OpenTelemetry setup for Effect applications.
3 *
4 * This module exports a `Configuration` type and layers for installing
5 * tracing, metrics, and logging in Node.js. The main `layer` builds the shared
6 * OpenTelemetry resource from environment variables and optional service
7 * metadata, then enables only the signal types that have processors or readers
8 * configured. `layerTracerProvider` creates a scoped Node tracer provider, and
9 * `layerEmpty` provides an empty resource.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import type * as Otel from "@opentelemetry/api"
15import type { LoggerProviderConfig, LogRecordProcessor } from "@opentelemetry/sdk-logs"
16import type { MetricReader } from "@opentelemetry/sdk-metrics"
17import type { SpanProcessor, TracerConfig } from "@opentelemetry/sdk-trace-base"
18import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
19import type { NonEmptyReadonlyArray } from "effect/Array"
20import type * as Duration from "effect/Duration"
21import * as Effect from "effect/Effect"
22import { constant, type LazyArg } from "effect/Function"
23import * as Layer from "effect/Layer"
24import { isNonEmpty } from "./internal/utilities.ts"
25import * as Logger from "./OtelLogger.ts"
26import * as Metrics from "./OtelMetrics.ts"
27import * as Tracer from "./OtelTracer.ts"
28import * as Resource from "./Resource.ts"
29
30/**
31 * Configuration for the Node OpenTelemetry layer, including optional tracing, metrics, logging, resource, and shutdown settings.
32 *
33 * @stability unstable
34 * @category models
35 * @since 4.0.0
36 */
37export interface Configuration {
38 readonly spanProcessor?: SpanProcessor | ReadonlyArray<SpanProcessor> | undefined
39 readonly tracerConfig?: Omit<TracerConfig, "resource"> | undefined
40 readonly metricReader?: MetricReader | ReadonlyArray<MetricReader> | undefined
41 readonly metricTemporality?: Metrics.TemporalityPreference | undefined
42 readonly logRecordProcessor?: LogRecordProcessor | ReadonlyArray<LogRecordProcessor> | undefined
43 readonly loggerProviderConfig?: Omit<LoggerProviderConfig, "resource"> | undefined
44 readonly loggerMergeWithExisting?: boolean | undefined
45 readonly resource?: {
46 readonly serviceName: string
47 readonly serviceVersion?: string
48 readonly attributes?: Otel.Attributes
49 } | undefined
50 readonly shutdownTimeout?: Duration.Input | undefined
51}
52
53/**
54 * Creates a scoped Node OpenTelemetry tracer provider from one or more span processors and shuts it down when the layer is released.
55 *
56 * @stability unstable
57 * @category layers
58 * @since 4.0.0
59 */
60export const layerTracerProvider = (
61 processor: SpanProcessor | NonEmptyReadonlyArray<SpanProcessor>,
62 config?: Omit<TracerConfig, "resource"> & {
63 readonly shutdownTimeout?: Duration.Input | undefined
64 }
65): Layer.Layer<Tracer.OtelTracerProvider, never, Resource.Resource> =>
66 Layer.effect(
67 Tracer.OtelTracerProvider,
68 Effect.gen(function*() {
69 const resource = yield* Resource.Resource
70 return yield* Effect.acquireRelease(
71 Effect.sync(() => {
72 const provider = new NodeTracerProvider({
73 ...(config ?? undefined),
74 resource,
75 spanProcessors: Array.isArray(processor) ? (processor as any) : [processor]
76 })
77 return provider
78 }),
79 (provider) =>
80 Effect.promise(() => provider.forceFlush().finally(() => provider.shutdown())).pipe(
81 Effect.ignore,
82 Effect.interruptible,
83 Effect.timeoutOption(config?.shutdownTimeout ?? 3000)
84 )
85 )
86 })
87 )
88
89/**
90 * Creates a Node OpenTelemetry layer from configuration, enabling tracing, metrics, and logging only when their processors or readers are supplied.
91 *
92 * **When to use**
93 *
94 * Use to install OpenTelemetry support for a Node.js Effect application from
95 * one configuration object, enabling tracing, metrics, logging, or any
96 * combination of those signals based on the processors and readers supplied.
97 *
98 * **Details**
99 *
100 * The configuration can be provided lazily or effectfully. The layer always
101 * provides `Resource.Resource`, building it from environment variables and any
102 * explicit resource metadata in the configuration.
103 *
104 * **Gotchas**
105 *
106 * Register Node auto-instrumentations before importing modules that should be
107 * patched, because many Node instrumentations hook module loading.
108 *
109 * @stability unstable
110 * @category layers
111 * @since 4.0.0
112 */
113export const layer: {
114 (evaluate: LazyArg<Configuration>): Layer.Layer<Resource.Resource>
115 <R, E>(evaluate: Effect.Effect<Configuration, E, R>): Layer.Layer<Resource.Resource, E, R>
116} = (
117 evaluate: LazyArg<Configuration> | Effect.Effect<Configuration, any, any>
118): Layer.Layer<Resource.Resource> =>
119 Layer.unwrap(
120 Effect.gen(function*() {
121 const config = yield* Effect.isEffect(evaluate)
122 ? evaluate as Effect.Effect<Configuration>
123 : Effect.sync(evaluate)
124
125 const ResourceLayer = Resource.layerFromEnv(config.resource && Resource.configToAttributes(config.resource))
126
127 const TracerLayer = isNonEmpty(config.spanProcessor)
128 ? Layer.provide(
129 Tracer.layer,
130 layerTracerProvider(config.spanProcessor, {
131 ...config.tracerConfig,
132 shutdownTimeout: config.shutdownTimeout
133 })
134 )
135 : Layer.empty
136
137 const MetricsLayer = isNonEmpty(config.metricReader)
138 ? Metrics.layer(constant(config.metricReader), {
139 shutdownTimeout: config.shutdownTimeout,
140 temporality: config.metricTemporality
141 })
142 : Layer.empty
143
144 const LoggerLayer = isNonEmpty(config.logRecordProcessor)
145 ? Layer.provide(
146 Logger.layer({ mergeWithExisting: config.loggerMergeWithExisting }),
147 Logger.layerLoggerProvider(config.logRecordProcessor, {
148 ...config.loggerProviderConfig,
149 shutdownTimeout: config.shutdownTimeout
150 })
151 )
152 : Layer.empty
153
154 return Layer.mergeAll(TracerLayer, MetricsLayer, LoggerLayer).pipe(
155 Layer.provideMerge(ResourceLayer)
156 )
157 })
158 )
159
160/**
161 * Layer that provides an empty OpenTelemetry `Resource`.
162 *
163 * @stability unstable
164 * @category layers
165 * @since 2.0.0
166 */
167export const layerEmpty: Layer.Layer<Resource.Resource> = Resource.layerEmpty