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