@logtape/otel 1.3.0-dev.379 → 1.3.0-dev.380

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mod.ts CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  import { diag, type DiagLogger, DiagLogLevel } from "@opentelemetry/api";
8
8
  import {
9
9
  type AnyValue,
10
+ type Logger as OTLogger,
10
11
  type LoggerProvider as LoggerProviderBase,
11
12
  type LogRecord as OTLogRecord,
12
13
  SeverityNumber,
13
14
  } from "@opentelemetry/api-logs";
14
- import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
15
15
  import type { OTLPExporterNodeConfigBase } from "@opentelemetry/otlp-exporter-base";
16
16
  import {
17
17
  defaultResource,
@@ -19,7 +19,6 @@ import {
19
19
  } from "@opentelemetry/resources";
20
20
  import {
21
21
  LoggerProvider,
22
- type LogRecordProcessor,
23
22
  SimpleLogRecordProcessor,
24
23
  } from "@opentelemetry/sdk-logs";
25
24
  import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
@@ -59,16 +58,78 @@ function getEnvironmentVariable(name: string): string | undefined {
59
58
  return undefined;
60
59
  }
61
60
 
61
+ /**
62
+ * Detects the OTLP protocol from environment variables.
63
+ * Priority:
64
+ * 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL`
65
+ * 2. `OTEL_EXPORTER_OTLP_PROTOCOL`
66
+ * 3. Default: `"http/json"` (for backward compatibility)
67
+ *
68
+ * @returns The detected OTLP protocol.
69
+ */
70
+ function detectOtlpProtocol(): OtlpProtocol {
71
+ const logsProtocol = getEnvironmentVariable(
72
+ "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL",
73
+ );
74
+ if (
75
+ logsProtocol === "grpc" ||
76
+ logsProtocol === "http/protobuf" ||
77
+ logsProtocol === "http/json"
78
+ ) {
79
+ return logsProtocol;
80
+ }
81
+
82
+ const protocol = getEnvironmentVariable("OTEL_EXPORTER_OTLP_PROTOCOL");
83
+ if (
84
+ protocol === "grpc" ||
85
+ protocol === "http/protobuf" ||
86
+ protocol === "http/json"
87
+ ) {
88
+ return protocol;
89
+ }
90
+
91
+ return "http/json";
92
+ }
93
+
94
+ /**
95
+ * Creates an OTLP log exporter based on the detected protocol.
96
+ * Uses dynamic imports to maintain browser compatibility when gRPC is not used.
97
+ * @param config Optional exporter configuration.
98
+ * @returns A promise that resolves to the appropriate OTLP log exporter.
99
+ */
100
+ async function createOtlpExporter(
101
+ config?: OTLPExporterNodeConfigBase,
102
+ // deno-lint-ignore no-explicit-any
103
+ ): Promise<any> {
104
+ const protocol = detectOtlpProtocol();
105
+
106
+ switch (protocol) {
107
+ case "grpc": {
108
+ const { OTLPLogExporter } = await import(
109
+ "@opentelemetry/exporter-logs-otlp-grpc"
110
+ );
111
+ return new OTLPLogExporter(config);
112
+ }
113
+ case "http/protobuf": {
114
+ const { OTLPLogExporter } = await import(
115
+ "@opentelemetry/exporter-logs-otlp-proto"
116
+ );
117
+ return new OTLPLogExporter(config);
118
+ }
119
+ case "http/json":
120
+ default: {
121
+ const { OTLPLogExporter } = await import(
122
+ "@opentelemetry/exporter-logs-otlp-http"
123
+ );
124
+ return new OTLPLogExporter(config);
125
+ }
126
+ }
127
+ }
128
+
62
129
  /**
63
130
  * The OpenTelemetry logger provider.
64
131
  */
65
132
  type ILoggerProvider = LoggerProviderBase & {
66
- /**
67
- * Adds a new {@link LogRecordProcessor} to this logger.
68
- * @param processor the new LogRecordProcessor to be added.
69
- */
70
- addLogRecordProcessor(processor: LogRecordProcessor): void;
71
-
72
133
  /**
73
134
  * Flush all buffered data and shut down the LoggerProvider and all registered
74
135
  * LogRecordProcessor.
@@ -95,14 +156,15 @@ type Message = (string | null | undefined)[];
95
156
  export type BodyFormatter = (message: Message) => AnyValue;
96
157
 
97
158
  /**
98
- * Options for creating an OpenTelemetry sink.
159
+ * The OTLP protocol to use for exporting logs.
160
+ * @since 0.9.0
99
161
  */
100
- export interface OpenTelemetrySinkOptions {
101
- /**
102
- * The OpenTelemetry logger provider to use.
103
- */
104
- loggerProvider?: ILoggerProvider;
162
+ export type OtlpProtocol = "grpc" | "http/protobuf" | "http/json";
105
163
 
164
+ /**
165
+ * Base options shared by all OpenTelemetry sink configurations.
166
+ */
167
+ interface OpenTelemetrySinkOptionsBase {
106
168
  /**
107
169
  * The way to render the message in the log record. If `"string"`,
108
170
  * the message is rendered as a single string with the values are
@@ -128,85 +190,260 @@ export interface OpenTelemetrySinkOptions {
128
190
  * Turned off by default.
129
191
  */
130
192
  diagnostics?: boolean;
193
+ }
194
+
195
+ /**
196
+ * Options for creating an OpenTelemetry sink with a custom logger provider.
197
+ * When using this configuration, you are responsible for setting up the
198
+ * logger provider with appropriate exporters and processors.
199
+ *
200
+ * This is the recommended approach for production use as it gives you
201
+ * full control over the OpenTelemetry configuration.
202
+ * @since 0.9.0
203
+ */
204
+ export interface OpenTelemetrySinkProviderOptions
205
+ extends OpenTelemetrySinkOptionsBase {
206
+ /**
207
+ * The OpenTelemetry logger provider to use.
208
+ */
209
+ loggerProvider: ILoggerProvider;
210
+ }
211
+
212
+ /**
213
+ * Options for creating an OpenTelemetry sink with automatic exporter creation.
214
+ * The protocol is determined by environment variables:
215
+ * - `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` (highest priority)
216
+ * - `OTEL_EXPORTER_OTLP_PROTOCOL` (fallback)
217
+ * - Default: `"http/json"`
218
+ *
219
+ * For production use, consider providing your own {@link ILoggerProvider}
220
+ * via {@link OpenTelemetrySinkProviderOptions} for more control.
221
+ * @since 0.9.0
222
+ */
223
+ export interface OpenTelemetrySinkExporterOptions
224
+ extends OpenTelemetrySinkOptionsBase {
225
+ /**
226
+ * The OpenTelemetry logger provider to use.
227
+ * Must be undefined or omitted when using exporter options.
228
+ */
229
+ loggerProvider?: undefined;
131
230
 
132
231
  /**
133
232
  * The OpenTelemetry OTLP exporter configuration to use.
134
- * Ignored if `loggerProvider` is provided.
135
233
  */
136
234
  otlpExporterConfig?: OTLPExporterNodeConfigBase;
137
235
 
138
236
  /**
139
237
  * The service name to use. If not provided, the service name is
140
238
  * taken from the `OTEL_SERVICE_NAME` environment variable.
141
- * Ignored if `loggerProvider` is provided.
142
239
  */
143
240
  serviceName?: string;
144
241
  }
145
242
 
243
+ /**
244
+ * Options for creating an OpenTelemetry sink.
245
+ *
246
+ * This is a union type that accepts either:
247
+ * - {@link OpenTelemetrySinkProviderOptions}: Provide your own `loggerProvider`
248
+ * (recommended for production)
249
+ * - {@link OpenTelemetrySinkExporterOptions}: Let the sink create an exporter
250
+ * automatically based on environment variables
251
+ *
252
+ * When no `loggerProvider` is provided, the protocol is determined by:
253
+ * 1. `OTEL_EXPORTER_OTLP_LOGS_PROTOCOL` environment variable
254
+ * 2. `OTEL_EXPORTER_OTLP_PROTOCOL` environment variable
255
+ * 3. Default: `"http/json"`
256
+ *
257
+ * @example Using a custom logger provider (recommended)
258
+ * ```typescript
259
+ * import { LoggerProvider, SimpleLogRecordProcessor } from "@opentelemetry/sdk-logs";
260
+ * import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-grpc";
261
+ *
262
+ * const provider = new LoggerProvider();
263
+ * provider.addLogRecordProcessor(new SimpleLogRecordProcessor(new OTLPLogExporter()));
264
+ *
265
+ * const sink = getOpenTelemetrySink({ loggerProvider: provider });
266
+ * ```
267
+ *
268
+ * @example Using automatic exporter creation
269
+ * ```typescript
270
+ * // Protocol determined by OTEL_EXPORTER_OTLP_PROTOCOL env var
271
+ * const sink = getOpenTelemetrySink({
272
+ * serviceName: "my-service",
273
+ * });
274
+ * ```
275
+ */
276
+ export type OpenTelemetrySinkOptions =
277
+ | OpenTelemetrySinkProviderOptions
278
+ | OpenTelemetrySinkExporterOptions;
279
+
280
+ /**
281
+ * Initializes the logger provider asynchronously.
282
+ * This is used when the user doesn't provide a custom logger provider.
283
+ * @param options The exporter options.
284
+ * @returns A promise that resolves to the initialized logger provider.
285
+ */
286
+ async function initializeLoggerProvider(
287
+ options: OpenTelemetrySinkExporterOptions,
288
+ ): Promise<ILoggerProvider> {
289
+ const resource = defaultResource().merge(
290
+ resourceFromAttributes({
291
+ [ATTR_SERVICE_NAME]: options.serviceName ??
292
+ getEnvironmentVariable("OTEL_SERVICE_NAME"),
293
+ }),
294
+ );
295
+ const otlpExporter = await createOtlpExporter(options.otlpExporterConfig);
296
+ const loggerProvider = new LoggerProvider({
297
+ resource,
298
+ processors: [
299
+ // @ts-ignore: it works anyway...
300
+ new SimpleLogRecordProcessor(otlpExporter),
301
+ ],
302
+ });
303
+ return loggerProvider;
304
+ }
305
+
306
+ /**
307
+ * Emits a log record to the OpenTelemetry logger.
308
+ * @param logger The OpenTelemetry logger.
309
+ * @param record The LogTape log record.
310
+ * @param options The sink options.
311
+ */
312
+ function emitLogRecord(
313
+ logger: OTLogger,
314
+ record: LogRecord,
315
+ options: OpenTelemetrySinkOptions,
316
+ ): void {
317
+ const objectRenderer = options.objectRenderer ?? "inspect";
318
+ const { category, level, message, timestamp, properties } = record;
319
+ const severityNumber = mapLevelToSeverityNumber(level);
320
+ const attributes = convertToAttributes(properties, objectRenderer);
321
+ attributes["category"] = [...category];
322
+ logger.emit(
323
+ {
324
+ severityNumber,
325
+ severityText: level,
326
+ body: typeof options.messageType === "function"
327
+ ? convertMessageToCustomBodyFormat(
328
+ message,
329
+ objectRenderer,
330
+ options.messageType,
331
+ )
332
+ : options.messageType === "array"
333
+ ? convertMessageToArray(message, objectRenderer)
334
+ : convertMessageToString(message, objectRenderer),
335
+ attributes,
336
+ timestamp: new Date(timestamp),
337
+ } satisfies OTLogRecord,
338
+ );
339
+ }
340
+
146
341
  /**
147
342
  * Creates a sink that forwards log records to OpenTelemetry.
343
+ *
344
+ * When a custom `loggerProvider` is provided, it is used directly.
345
+ * Otherwise, the sink will lazily initialize a logger provider on the first
346
+ * log record, using the protocol determined by environment variables.
347
+ *
148
348
  * @param options Options for creating the sink.
149
349
  * @returns The sink.
150
350
  */
151
351
  export function getOpenTelemetrySink(
152
352
  options: OpenTelemetrySinkOptions = {},
153
- ): Sink {
353
+ ): Sink & AsyncDisposable {
154
354
  if (options.diagnostics) {
155
355
  diag.setLogger(new DiagLoggerAdaptor(), DiagLogLevel.DEBUG);
156
356
  }
157
357
 
158
- let loggerProvider: ILoggerProvider;
159
- if (options.loggerProvider == null) {
160
- const resource = defaultResource().merge(
161
- resourceFromAttributes({
162
- [ATTR_SERVICE_NAME]: options.serviceName ??
163
- getEnvironmentVariable("OTEL_SERVICE_NAME"),
164
- }),
165
- );
166
- loggerProvider = new LoggerProvider({ resource });
167
- const otlpExporter = new OTLPLogExporter(options.otlpExporterConfig);
168
- loggerProvider.addLogRecordProcessor(
169
- // @ts-ignore: it works anyway...
170
- new SimpleLogRecordProcessor(otlpExporter),
171
- );
172
- } else {
173
- loggerProvider = options.loggerProvider;
174
- }
175
- const objectRenderer = options.objectRenderer ?? "inspect";
176
- const logger = loggerProvider.getLogger(metadata.name, metadata.version);
177
- const sink = (record: LogRecord) => {
178
- const { category, level, message, timestamp, properties } = record;
179
- if (
180
- category[0] === "logtape" && category[1] === "meta" &&
181
- category[2] === "otel"
182
- ) {
183
- return;
184
- }
185
- const severityNumber = mapLevelToSeverityNumber(level);
186
- const attributes = convertToAttributes(properties, objectRenderer);
187
- attributes["category"] = [...category];
188
- logger.emit(
358
+ // If loggerProvider is provided, use the synchronous path
359
+ if (options.loggerProvider != null) {
360
+ const loggerProvider = options.loggerProvider;
361
+ const logger = loggerProvider.getLogger(metadata.name, metadata.version);
362
+ const shutdown = loggerProvider.shutdown?.bind(loggerProvider);
363
+ const sink: Sink & AsyncDisposable = Object.assign(
364
+ (record: LogRecord) => {
365
+ const { category } = record;
366
+ if (
367
+ category[0] === "logtape" && category[1] === "meta" &&
368
+ category[2] === "otel"
369
+ ) {
370
+ return;
371
+ }
372
+ emitLogRecord(logger, record, options);
373
+ },
189
374
  {
190
- severityNumber,
191
- severityText: level,
192
- body: typeof options.messageType === "function"
193
- ? convertMessageToCustomBodyFormat(
194
- message,
195
- objectRenderer,
196
- options.messageType,
197
- )
198
- : options.messageType === "array"
199
- ? convertMessageToArray(message, objectRenderer)
200
- : convertMessageToString(message, objectRenderer),
201
- attributes,
202
- timestamp: new Date(timestamp),
203
- } satisfies OTLogRecord,
375
+ async [Symbol.asyncDispose](): Promise<void> {
376
+ if (shutdown != null) await shutdown();
377
+ },
378
+ },
204
379
  );
205
- };
206
- if (loggerProvider.shutdown != null) {
207
- const shutdown = loggerProvider.shutdown.bind(loggerProvider);
208
- sink[Symbol.asyncDispose] = shutdown;
380
+ return sink;
209
381
  }
382
+
383
+ // Lazy initialization for automatic exporter creation
384
+ let loggerProvider: ILoggerProvider | null = null;
385
+ let logger: OTLogger | null = null;
386
+ let initPromise: Promise<void> | null = null;
387
+ let initError: Error | null = null;
388
+
389
+ const sink: Sink & AsyncDisposable = Object.assign(
390
+ (record: LogRecord) => {
391
+ const { category } = record;
392
+ if (
393
+ category[0] === "logtape" && category[1] === "meta" &&
394
+ category[2] === "otel"
395
+ ) {
396
+ return;
397
+ }
398
+
399
+ // If already initialized, emit the log
400
+ if (logger != null) {
401
+ emitLogRecord(logger, record, options);
402
+ return;
403
+ }
404
+
405
+ // If initialization failed, skip silently
406
+ if (initError != null) {
407
+ return;
408
+ }
409
+
410
+ // Start initialization if not already started
411
+ if (initPromise == null) {
412
+ initPromise = initializeLoggerProvider(options)
413
+ .then((provider) => {
414
+ loggerProvider = provider;
415
+ logger = provider.getLogger(metadata.name, metadata.version);
416
+ // Emit the current record that triggered initialization
417
+ emitLogRecord(logger, record, options);
418
+ })
419
+ .catch((error) => {
420
+ initError = error;
421
+ // Log initialization error to console as a fallback
422
+ // deno-lint-ignore no-console
423
+ console.error("Failed to initialize OpenTelemetry logger:", error);
424
+ });
425
+ }
426
+ // Records during initialization are dropped
427
+ // (the triggering record is emitted in the then() callback above)
428
+ },
429
+ {
430
+ async [Symbol.asyncDispose](): Promise<void> {
431
+ // Wait for initialization to complete if in progress
432
+ if (initPromise != null) {
433
+ try {
434
+ await initPromise;
435
+ } catch {
436
+ // Initialization failed, nothing to shut down
437
+ return;
438
+ }
439
+ }
440
+ if (loggerProvider?.shutdown != null) {
441
+ await loggerProvider.shutdown();
442
+ }
443
+ },
444
+ },
445
+ );
446
+
210
447
  return sink;
211
448
  }
212
449