@logtape/otel 1.3.0-dev.378 → 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/deno.json +9 -7
- package/dist/deno.cjs +9 -7
- package/dist/deno.js +9 -7
- package/dist/deno.js.map +1 -1
- package/dist/mod.cjs +119 -26
- package/dist/mod.d.cts +81 -16
- package/dist/mod.d.cts.map +1 -1
- package/dist/mod.d.ts +81 -16
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +119 -26
- package/dist/mod.js.map +1 -1
- package/package.json +10 -8
- package/src/mod.test.ts +571 -2
- package/src/mod.ts +303 -66
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
|
-
*
|
|
159
|
+
* The OTLP protocol to use for exporting logs.
|
|
160
|
+
* @since 0.9.0
|
|
99
161
|
*/
|
|
100
|
-
export
|
|
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
|
-
|
|
159
|
-
if (options.loggerProvider
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|