1/**
2 * Connects Effect logging to the OpenTelemetry Logs SDK.
3 *
4 * This module turns Effect log events into OpenTelemetry log records. It maps
5 * Effect log levels to OpenTelemetry severity numbers, provides the
6 * `OtelLoggerProvider` service, creates an Effect `Logger` with `make`, and
7 * offers layers for installing that logger or creating a scoped SDK
8 * `LoggerProvider` from one or more `LogRecordProcessor`s.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import { SeverityNumber } from "@opentelemetry/api-logs"
14import * as Otel from "@opentelemetry/sdk-logs"
15import type { NonEmptyReadonlyArray } from "effect/Array"
16import * as Arr from "effect/Array"
17import * as Clock from "effect/Clock"
18import * as Context from "effect/Context"
19import type * as Duration from "effect/Duration"
20import * as Effect from "effect/Effect"
21import * as Layer from "effect/Layer"
22import * as Logger from "effect/Logger"
23import type * as LogLevel from "effect/LogLevel"
24import * as Predicate from "effect/Predicate"
25import * as Rec from "effect/Record"
26import * as References from "effect/References"
27import * as Tracer from "effect/Tracer"
28import { nanosToHrTime, unknownToAttributeValue } from "./internal/attributes.ts"
29import { Resource } from "./Resource.ts"
30
31/**
32 * Context service containing the OpenTelemetry `LoggerProvider` used to emit Effect log records.
33 *
34 * @stability unstable
35 * @category services
36 * @since 4.0.0
37 */
38export class OtelLoggerProvider extends Context.Service<
39 OtelLoggerProvider,
40 Otel.LoggerProvider
41>()("@effect/opentelemetry/Logger/OtelLoggerProvider") {}
42
43/**
44 * Maps an Effect `LogLevel` to the corresponding OpenTelemetry `SeverityNumber`.
45 *
46 * **Details**
47 *
48 * OpenTelemetry log severity numbers are in the range `1` through `24`. This
49 * function maps from Effect's log levels instead of using
50 * `LogLevel.getOrdinal`, whose internal sort ordinals, such as the `Info`
51 * ordinal `20000`, fall outside the OpenTelemetry logs data model and can be
52 * treated as `UNSPECIFIED` by validating backends.
53 *
54 * @stability unstable
55 * @category converting
56 * @since 4.0.0
57 */
58export const logLevelToSeverityNumber = (level: LogLevel.LogLevel): SeverityNumber => {
59 switch (level) {
60 case "Trace":
61 return SeverityNumber.TRACE
62 case "Debug":
63 return SeverityNumber.DEBUG
64 case "Info":
65 return SeverityNumber.INFO
66 case "Warn":
67 return SeverityNumber.WARN
68 case "Error":
69 return SeverityNumber.ERROR
70 case "Fatal":
71 return SeverityNumber.FATAL
72 default:
73 return SeverityNumber.UNSPECIFIED
74 }
75}
76
77/**
78 * Creates an Effect logger that emits log records through the configured OpenTelemetry logger provider.
79 *
80 * @stability unstable
81 * @category constructors
82 * @since 4.0.0
83 */
84export const make: Effect.Effect<
85 Logger.Logger<unknown, void>,
86 never,
87 OtelLoggerProvider
88> = Effect.gen(function*() {
89 const loggerProvider = yield* OtelLoggerProvider
90 const clock = yield* Clock.Clock
91 const otelLogger = loggerProvider.getLogger("@effect/opentelemetry")
92
93 return Logger.make((options) => {
94 const attributes: Record<string, any> = {
95 fiberId: options.fiber.id
96 }
97
98 for (const [key, value] of Object.entries(options.fiber.getRef(References.CurrentLogAnnotations))) {
99 Rec.assignProperty(attributes, key, unknownToAttributeValue(value))
100 }
101
102 const span = Context.getOrUndefined(options.fiber.context, Tracer.ParentSpan)
103
104 if (Predicate.isNotUndefined(span)) {
105 attributes.spanId = span.spanId
106 attributes.traceId = span.traceId
107 }
108
109 const now = options.date.getTime()
110 for (const [label, startTime] of options.fiber.getRef(References.CurrentLogSpans)) {
111 attributes[`logSpan.${label}`] = `${now - startTime}ms`
112 }
113
114 const message = Arr.ensure(options.message).map(unknownToAttributeValue)
115 const hrTime = nanosToHrTime(clock.currentTimeNanosUnsafe())
116 otelLogger.emit({
117 body: message.length === 1 ? message[0] : message,
118 severityText: options.logLevel,
119 severityNumber: logLevelToSeverityNumber(options.logLevel),
120 timestamp: hrTime,
121 observedTimestamp: hrTime,
122 attributes
123 })
124 })
125})
126
127/**
128 * Creates a layer that installs the OpenTelemetry-backed Effect logger, merging with existing loggers by default.
129 *
130 * **When to use**
131 *
132 * Use to install the OpenTelemetry-backed Effect logger in an application that
133 * has an `OtelLoggerProvider`, so standard Effect logging emits OpenTelemetry
134 * log records.
135 *
136 * **Details**
137 *
138 * The layer installs the logger created by `make`. `mergeWithExisting` defaults
139 * to `true`; set it to `false` to replace the current logger set.
140 *
141 * @see {@link make} for constructing the logger directly
142 * @see {@link layerLoggerProvider} for creating the required logger provider
143 *
144 * @stability unstable
145 * @category layers
146 * @since 4.0.0
147 */
148export const layer = (options: {
149 /**
150 * Whether to merge the OpenTelemetry logger with existing loggers.
151 *
152 * **Details**
153 *
154 * If set to `true`, the OpenTelemetry logger will be merged with existing
155 * loggers in the application.
156 *
157 * If set to `false`, the OpenTelemetry logger will replace all existing
158 * loggers in the application.
159 *
160 * Defaults to `true`.
161 */
162 readonly mergeWithExisting?: boolean | undefined
163}): Layer.Layer<never, never, OtelLoggerProvider> =>
164 Logger.layer([make], {
165 mergeWithExisting: options.mergeWithExisting ?? true
166 })
167
168/**
169 * Creates a scoped OpenTelemetry logger provider from one or more log record processors, using the current `Resource` and flushing and shutting down the provider when the layer is released.
170 *
171 * @stability unstable
172 * @category layers
173 * @since 4.0.0
174 */
175export const layerLoggerProvider = (
176 processor: Otel.LogRecordProcessor | NonEmptyReadonlyArray<Otel.LogRecordProcessor>,
177 config?: Omit<Otel.LoggerProviderConfig, "resource"> & {
178 readonly shutdownTimeout?: Duration.Input | undefined
179 }
180): Layer.Layer<OtelLoggerProvider, never, Resource> =>
181 Layer.effect(
182 OtelLoggerProvider,
183 Effect.gen(function*() {
184 const resource = yield* Resource
185 return yield* Effect.acquireRelease(
186 Effect.sync(() =>
187 new Otel.LoggerProvider({
188 ...(config ?? undefined),
189 processors: Arr.ensure(processor),
190 resource
191 })
192 ),
193 (provider) =>
194 Effect.promise(() => provider.forceFlush().finally(() => provider.shutdown())).pipe(
195 Effect.ignore,
196 Effect.interruptible,
197 Effect.timeoutOption(config?.shutdownTimeout ?? 3000)
198 )
199 )
200 })
201 )