| 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 | */ |
| 13 | import type * as Otel from "@opentelemetry/api" |
| 14 | import type { LoggerProviderConfig, LogRecordProcessor } from "@opentelemetry/sdk-logs" |
| 15 | import type { MetricReader } from "@opentelemetry/sdk-metrics" |
| 16 | import type { SpanProcessor, TracerConfig } from "@opentelemetry/sdk-trace-base" |
| 17 | import { WebTracerProvider } from "@opentelemetry/sdk-trace-web" |
| 18 | import type { NonEmptyReadonlyArray } from "effect/Array" |
| 19 | import * as Effect from "effect/Effect" |
| 20 | import { constant, type LazyArg } from "effect/Function" |
| 21 | import * as Layer from "effect/Layer" |
| 22 | import { isNonEmpty } from "./internal/utilities.ts" |
| 23 | import * as Logger from "./OtelLogger.ts" |
| 24 | import * as Metrics from "./OtelMetrics.ts" |
| 25 | import * as Tracer from "./OtelTracer.ts" |
| 26 | import * 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 | */ |
| 35 | export 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 | */ |
| 57 | export 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 | */ |
| 107 | export 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 | ) |