@devopsplaybook.io/otel-utils 1.7.0 → 1.8.0
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/README.md +55 -15
- package/dist/src/ModuleLogger.d.ts +7 -2
- package/dist/src/ModuleLogger.js +65 -6
- package/dist/src/StandardLogger.d.ts +2 -1
- package/dist/src/StandardLogger.js +11 -4
- package/dist/src/StandardMeter.js +3 -2
- package/dist/src/StandardTracer.js +2 -0
- package/dist/src/models/ConfigOTelInterface.d.ts +5 -0
- package/dist/src/utils/configValidation.d.ts +5 -0
- package/dist/src/utils/configValidation.js +36 -0
- package/package.json +1 -1
- package/src/ModuleLogger.spec.ts +169 -17
- package/src/ModuleLogger.ts +84 -7
- package/src/StandardLogger.spec.ts +243 -0
- package/src/StandardLogger.ts +34 -5
- package/src/StandardMeter.spec.ts +105 -0
- package/src/StandardMeter.ts +9 -2
- package/src/StandardTracer.spec.ts +14 -0
- package/src/StandardTracer.ts +2 -0
- package/src/models/ConfigOTelInterface.ts +6 -0
- package/src/utils/configValidation.spec.ts +141 -0
- package/src/utils/configValidation.ts +50 -0
package/src/ModuleLogger.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { SeverityNumber } from "@opentelemetry/api";
|
|
2
2
|
import type { Span } from "@opentelemetry/api";
|
|
3
|
+
import type { ConsoleLogMode } from "./models/ConfigOTelInterface";
|
|
3
4
|
import { StandardLoggerInterface } from "./models/StandardLoggerInterface";
|
|
4
5
|
|
|
6
|
+
export interface ModuleLoggerOptions {
|
|
7
|
+
consoleMode?: ConsoleLogMode;
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export class ModuleLogger {
|
|
6
11
|
private module: string;
|
|
7
12
|
private standardLogger: StandardLoggerInterface;
|
|
13
|
+
private consoleMode: ConsoleLogMode;
|
|
8
14
|
|
|
9
|
-
constructor(
|
|
15
|
+
constructor(
|
|
16
|
+
module: string,
|
|
17
|
+
standardLogger: StandardLoggerInterface,
|
|
18
|
+
options: ModuleLoggerOptions = {},
|
|
19
|
+
) {
|
|
10
20
|
this.module = module;
|
|
11
21
|
this.standardLogger = standardLogger;
|
|
22
|
+
this.consoleMode = options.consoleMode ?? "always";
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
public info(message: string, context?: Span): void {
|
|
@@ -19,8 +30,18 @@ export class ModuleLogger {
|
|
|
19
30
|
this.display("warn", message, SeverityNumber.WARN, null, context);
|
|
20
31
|
}
|
|
21
32
|
|
|
22
|
-
public error(message: string, error?:
|
|
23
|
-
|
|
33
|
+
public error(message: string, error?: unknown, context?: Span): void {
|
|
34
|
+
if (!context && isSpanLike(error)) {
|
|
35
|
+
context = error;
|
|
36
|
+
error = undefined;
|
|
37
|
+
}
|
|
38
|
+
this.display(
|
|
39
|
+
"error",
|
|
40
|
+
message,
|
|
41
|
+
SeverityNumber.ERROR,
|
|
42
|
+
normalizeError(error),
|
|
43
|
+
context,
|
|
44
|
+
);
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
private display(
|
|
@@ -30,13 +51,17 @@ export class ModuleLogger {
|
|
|
30
51
|
error?: Error | null,
|
|
31
52
|
context?: Span,
|
|
32
53
|
): void {
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
const attributes: Record<string, string> = {
|
|
55
|
+
"log.type": "custom",
|
|
56
|
+
"log.module": this.module,
|
|
57
|
+
};
|
|
35
58
|
|
|
36
59
|
if (error) {
|
|
37
60
|
attributes["exception.type"] = error.name;
|
|
38
61
|
attributes["exception.message"] = error.message;
|
|
39
|
-
|
|
62
|
+
if (error.stack !== undefined) {
|
|
63
|
+
attributes["exception.stacktrace"] = error.stack;
|
|
64
|
+
}
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
if (context) {
|
|
@@ -45,8 +70,13 @@ export class ModuleLogger {
|
|
|
45
70
|
attributes["trace.id"] = spanCtx.traceId;
|
|
46
71
|
}
|
|
47
72
|
|
|
48
|
-
console.log(`[${level}] [${this.module}] ${message}`);
|
|
49
73
|
const logger = this.standardLogger.getLogger();
|
|
74
|
+
const mirrorToConsole =
|
|
75
|
+
this.consoleMode === "always" ||
|
|
76
|
+
(this.consoleMode === "fallback" && !logger);
|
|
77
|
+
if (mirrorToConsole) {
|
|
78
|
+
writeToConsole(level, `[${level}] [${this.module}] ${message}`);
|
|
79
|
+
}
|
|
50
80
|
if (!logger) {
|
|
51
81
|
return;
|
|
52
82
|
}
|
|
@@ -58,3 +88,50 @@ export class ModuleLogger {
|
|
|
58
88
|
});
|
|
59
89
|
}
|
|
60
90
|
}
|
|
91
|
+
|
|
92
|
+
function normalizeError(value: unknown): Error | null {
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (value instanceof Error) {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
return new Error(serializeUnknown(value));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function serializeUnknown(value: unknown): string {
|
|
103
|
+
if (typeof value === "string") {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const json = JSON.stringify(value);
|
|
108
|
+
if (json !== undefined) {
|
|
109
|
+
return json;
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Not serialisable; fall back to String below.
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
return String(value);
|
|
116
|
+
} catch {
|
|
117
|
+
return "Unknown error";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isSpanLike(value: unknown): value is Span {
|
|
122
|
+
return (
|
|
123
|
+
typeof value === "object" &&
|
|
124
|
+
value !== null &&
|
|
125
|
+
typeof (value as { spanContext?: unknown }).spanContext === "function"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function writeToConsole(level: string, line: string): void {
|
|
130
|
+
if (level === "error") {
|
|
131
|
+
console.error(line);
|
|
132
|
+
} else if (level === "warn") {
|
|
133
|
+
console.warn(line);
|
|
134
|
+
} else {
|
|
135
|
+
console.log(line);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import { logs, SeverityNumber } from "@opentelemetry/api";
|
|
2
2
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
3
3
|
import { LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
4
|
+
import type { ConsoleLogMode } from "./models/ConfigOTelInterface";
|
|
4
5
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
5
6
|
import { StandardLogger } from "./StandardLogger";
|
|
6
7
|
|
|
8
|
+
jest.mock("@opentelemetry/sdk-logs", () => {
|
|
9
|
+
const actual =
|
|
10
|
+
jest.requireActual<typeof import("@opentelemetry/sdk-logs")>(
|
|
11
|
+
"@opentelemetry/sdk-logs",
|
|
12
|
+
);
|
|
13
|
+
const batchProcessorOptions: unknown[] = [];
|
|
14
|
+
class RecordingBatchLogRecordProcessor extends actual.BatchLogRecordProcessor {
|
|
15
|
+
constructor(
|
|
16
|
+
options: ConstructorParameters<typeof actual.BatchLogRecordProcessor>[0],
|
|
17
|
+
) {
|
|
18
|
+
super(options);
|
|
19
|
+
batchProcessorOptions.push(options);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
...actual,
|
|
24
|
+
BatchLogRecordProcessor: RecordingBatchLogRecordProcessor,
|
|
25
|
+
__batchProcessorOptions: batchProcessorOptions,
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
interface SdkLogsMock {
|
|
30
|
+
__batchProcessorOptions: unknown[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function batchProcessorOptions(): unknown[] {
|
|
34
|
+
return (jest.requireMock("@opentelemetry/sdk-logs") as SdkLogsMock)
|
|
35
|
+
.__batchProcessorOptions;
|
|
36
|
+
}
|
|
37
|
+
|
|
7
38
|
jest.mock("@opentelemetry/exporter-logs-otlp-http", () => {
|
|
8
39
|
const { ExportResultCode } = jest.requireActual<
|
|
9
40
|
typeof import("@opentelemetry/core")
|
|
@@ -68,6 +99,7 @@ describe("StandardLogger", () => {
|
|
|
68
99
|
|
|
69
100
|
beforeEach(() => {
|
|
70
101
|
jest.clearAllMocks();
|
|
102
|
+
batchProcessorOptions().length = 0;
|
|
71
103
|
logs.disable();
|
|
72
104
|
warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
73
105
|
logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
@@ -233,4 +265,215 @@ describe("StandardLogger", () => {
|
|
|
233
265
|
logs.getLogger("external-lib").emit({ body: "dropped" }),
|
|
234
266
|
).not.toThrow();
|
|
235
267
|
});
|
|
268
|
+
|
|
269
|
+
describe("batch processor tunables", () => {
|
|
270
|
+
it("passes the documented defaults to BatchLogRecordProcessor", async () => {
|
|
271
|
+
const logger = makeLogger();
|
|
272
|
+
await logger.initOTel(collectorConfig("http://collector/v1/logs"));
|
|
273
|
+
|
|
274
|
+
expect(batchProcessorOptions()).toHaveLength(1);
|
|
275
|
+
expect(batchProcessorOptions()[0]).toEqual({
|
|
276
|
+
exporter: expect.anything(),
|
|
277
|
+
maxQueueSize: 2048,
|
|
278
|
+
maxExportBatchSize: 512,
|
|
279
|
+
exportTimeoutMillis: 30000,
|
|
280
|
+
scheduledDelayMillis: 60000,
|
|
281
|
+
});
|
|
282
|
+
await logger.shutdown();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("passes configured batch values to BatchLogRecordProcessor", async () => {
|
|
286
|
+
const logger = makeLogger();
|
|
287
|
+
await logger.initOTel({
|
|
288
|
+
...collectorConfig("http://collector/v1/logs"),
|
|
289
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS: 5,
|
|
290
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE: 4096,
|
|
291
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE: 1024,
|
|
292
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS: 15000,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(batchProcessorOptions()[0]).toEqual({
|
|
296
|
+
exporter: expect.anything(),
|
|
297
|
+
maxQueueSize: 4096,
|
|
298
|
+
maxExportBatchSize: 1024,
|
|
299
|
+
exportTimeoutMillis: 15000,
|
|
300
|
+
scheduledDelayMillis: 5000,
|
|
301
|
+
});
|
|
302
|
+
await logger.shutdown();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("falls back to the documented defaults for invalid batch values", async () => {
|
|
306
|
+
const logger = makeLogger();
|
|
307
|
+
await logger.initOTel({
|
|
308
|
+
...collectorConfig("http://collector/v1/logs"),
|
|
309
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE: 0,
|
|
310
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE: -1,
|
|
311
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS: 1.5,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(batchProcessorOptions()[0]).toEqual({
|
|
315
|
+
exporter: expect.anything(),
|
|
316
|
+
maxQueueSize: 2048,
|
|
317
|
+
maxExportBatchSize: 512,
|
|
318
|
+
exportTimeoutMillis: 30000,
|
|
319
|
+
scheduledDelayMillis: 60000,
|
|
320
|
+
});
|
|
321
|
+
await logger.shutdown();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it.each([0, -5, 0.5])(
|
|
325
|
+
"clamps the log export interval of %p seconds to one second",
|
|
326
|
+
async (interval) => {
|
|
327
|
+
const logger = makeLogger();
|
|
328
|
+
await logger.initOTel({
|
|
329
|
+
...collectorConfig("http://collector/v1/logs"),
|
|
330
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS: interval,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
expect(
|
|
334
|
+
(batchProcessorOptions()[0] as { scheduledDelayMillis: number })
|
|
335
|
+
.scheduledDelayMillis,
|
|
336
|
+
).toBe(1000);
|
|
337
|
+
await logger.shutdown();
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
it("falls back to the default delay for a NaN interval", async () => {
|
|
342
|
+
const logger = makeLogger();
|
|
343
|
+
await logger.initOTel({
|
|
344
|
+
...collectorConfig("http://collector/v1/logs"),
|
|
345
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS: NaN,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(
|
|
349
|
+
(batchProcessorOptions()[0] as { scheduledDelayMillis: number })
|
|
350
|
+
.scheduledDelayMillis,
|
|
351
|
+
).toBe(60000);
|
|
352
|
+
await logger.shutdown();
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("service identity validation", () => {
|
|
357
|
+
it("rejects an empty SERVICE_ID without creating an exporter", async () => {
|
|
358
|
+
const logger = makeLogger();
|
|
359
|
+
|
|
360
|
+
await expect(
|
|
361
|
+
logger.initOTel({
|
|
362
|
+
SERVICE_ID: " ",
|
|
363
|
+
VERSION: "1.0.0",
|
|
364
|
+
OPENTELEMETRY_COLLECTOR_HTTP_LOGS: "http://collector/v1/logs",
|
|
365
|
+
}),
|
|
366
|
+
).rejects.toThrow("[otel-utils] SERVICE_ID must be a non-empty string");
|
|
367
|
+
|
|
368
|
+
expect(mockExporterConstructor).not.toHaveBeenCalled();
|
|
369
|
+
expect(logger.getLogger()).toBeUndefined();
|
|
370
|
+
await expect(logger.shutdown()).resolves.toBeUndefined();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("rejects an empty VERSION without creating an exporter", async () => {
|
|
374
|
+
const logger = makeLogger();
|
|
375
|
+
|
|
376
|
+
await expect(
|
|
377
|
+
logger.initOTel({
|
|
378
|
+
SERVICE_ID: "log-service",
|
|
379
|
+
VERSION: "",
|
|
380
|
+
OPENTELEMETRY_COLLECTOR_HTTP_LOGS: "http://collector/v1/logs",
|
|
381
|
+
}),
|
|
382
|
+
).rejects.toThrow("[otel-utils] VERSION must be a non-empty string");
|
|
383
|
+
|
|
384
|
+
expect(mockExporterConstructor).not.toHaveBeenCalled();
|
|
385
|
+
await expect(logger.shutdown()).resolves.toBeUndefined();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("keeps the previous provider when a re-initialisation is invalid", async () => {
|
|
389
|
+
const logger = makeLogger();
|
|
390
|
+
await logger.initOTel(collectorConfig("http://collector-1/v1/logs"));
|
|
391
|
+
logger.createModuleLogger("billing").info("before-invalid-reinit");
|
|
392
|
+
|
|
393
|
+
await expect(
|
|
394
|
+
logger.initOTel({
|
|
395
|
+
SERVICE_ID: "",
|
|
396
|
+
VERSION: "1.0.0",
|
|
397
|
+
OPENTELEMETRY_COLLECTOR_HTTP_LOGS: "http://collector-2/v1/logs",
|
|
398
|
+
}),
|
|
399
|
+
).rejects.toThrow("[otel-utils] SERVICE_ID must be a non-empty string");
|
|
400
|
+
|
|
401
|
+
expect(logger.getLogger()).toBeDefined();
|
|
402
|
+
expect(logs.getLoggerProvider()).toBeInstanceOf(LoggerProvider);
|
|
403
|
+
logger.createModuleLogger("billing").info("after-invalid-reinit");
|
|
404
|
+
await logger.shutdown();
|
|
405
|
+
|
|
406
|
+
expect(mockExporterConstructor).toHaveBeenCalledTimes(1);
|
|
407
|
+
expect(exportedBodies(exporterAt(0))).toEqual(
|
|
408
|
+
expect.arrayContaining([
|
|
409
|
+
"[billing] before-invalid-reinit",
|
|
410
|
+
"[billing] after-invalid-reinit",
|
|
411
|
+
]),
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe("console log modes", () => {
|
|
417
|
+
it("mirrors through console.log by default", async () => {
|
|
418
|
+
const logger = makeLogger();
|
|
419
|
+
await logger.initOTel(collectorConfig("http://collector/v1/logs"));
|
|
420
|
+
|
|
421
|
+
logger.createModuleLogger("billing").info("mirrored");
|
|
422
|
+
|
|
423
|
+
expect(logSpy).toHaveBeenCalledWith("[info] [billing] mirrored");
|
|
424
|
+
await logger.shutdown();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it("never suppresses console output while records are still exported", async () => {
|
|
428
|
+
const logger = makeLogger();
|
|
429
|
+
await logger.initOTel({
|
|
430
|
+
...collectorConfig("http://collector/v1/logs"),
|
|
431
|
+
OPENTELEMETRY_CONSOLE_LOG_MODE: "never",
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
logger.createModuleLogger("billing").info("silent");
|
|
435
|
+
|
|
436
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
437
|
+
await logger.shutdown();
|
|
438
|
+
expect(exportedBodies(exporterAt(0))).toContain("[billing] silent");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("warns once and behaves as always for an invalid mode", async () => {
|
|
442
|
+
const logger = makeLogger();
|
|
443
|
+
await logger.initOTel({
|
|
444
|
+
...collectorConfig("http://collector/v1/logs"),
|
|
445
|
+
OPENTELEMETRY_CONSOLE_LOG_MODE:
|
|
446
|
+
"sometimes" as unknown as ConsoleLogMode,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
450
|
+
expect.stringContaining("OPENTELEMETRY_CONSOLE_LOG_MODE"),
|
|
451
|
+
);
|
|
452
|
+
logger.createModuleLogger("billing").info("still mirrored");
|
|
453
|
+
expect(logSpy).toHaveBeenCalledWith("[info] [billing] still mirrored");
|
|
454
|
+
await logger.shutdown();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("fallback mirrors only when no logs endpoint is configured", async () => {
|
|
458
|
+
const withEndpoint = makeLogger();
|
|
459
|
+
await withEndpoint.initOTel({
|
|
460
|
+
...collectorConfig("http://collector/v1/logs"),
|
|
461
|
+
OPENTELEMETRY_CONSOLE_LOG_MODE: "fallback",
|
|
462
|
+
});
|
|
463
|
+
withEndpoint.createModuleLogger("billing").info("not-mirrored");
|
|
464
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
465
|
+
await withEndpoint.shutdown();
|
|
466
|
+
expect(exportedBodies(exporterAt(0))).toContain("[billing] not-mirrored");
|
|
467
|
+
|
|
468
|
+
const withoutEndpoint = makeLogger();
|
|
469
|
+
await withoutEndpoint.initOTel({
|
|
470
|
+
SERVICE_ID: "log-service",
|
|
471
|
+
VERSION: "1.0.0",
|
|
472
|
+
OPENTELEMETRY_CONSOLE_LOG_MODE: "fallback",
|
|
473
|
+
});
|
|
474
|
+
withoutEndpoint.createModuleLogger("billing").warn("mirrored-fallback");
|
|
475
|
+
expect(warnSpy).toHaveBeenCalledWith("[warn] [billing] mirrored-fallback");
|
|
476
|
+
await withoutEndpoint.shutdown();
|
|
477
|
+
});
|
|
478
|
+
});
|
|
236
479
|
});
|
package/src/StandardLogger.ts
CHANGED
|
@@ -4,8 +4,17 @@ import {
|
|
|
4
4
|
BatchLogRecordProcessor,
|
|
5
5
|
LoggerProvider,
|
|
6
6
|
} from "@opentelemetry/sdk-logs";
|
|
7
|
-
import {
|
|
7
|
+
import type {
|
|
8
|
+
ConfigOTelInterface,
|
|
9
|
+
ConsoleLogMode,
|
|
10
|
+
} from "./models/ConfigOTelInterface";
|
|
8
11
|
import { createOTelResource } from "./utils/createResource";
|
|
12
|
+
import {
|
|
13
|
+
normalizeConsoleLogMode,
|
|
14
|
+
normalizeExportIntervalSeconds,
|
|
15
|
+
normalizePositiveInteger,
|
|
16
|
+
validateServiceIdentity,
|
|
17
|
+
} from "./utils/configValidation";
|
|
9
18
|
import { ModuleLogger } from "./ModuleLogger";
|
|
10
19
|
|
|
11
20
|
import type { StandardLoggerInterface } from "./models/StandardLoggerInterface";
|
|
@@ -22,8 +31,15 @@ export class StandardLogger implements StandardLoggerInterface {
|
|
|
22
31
|
private serviceName?: string;
|
|
23
32
|
private ownsGlobalProvider = false;
|
|
24
33
|
private shutdownPromise?: Promise<void>;
|
|
34
|
+
private consoleMode: ConsoleLogMode = "always";
|
|
25
35
|
|
|
26
36
|
public async initOTel(config: ConfigOTelInterface): Promise<void> {
|
|
37
|
+
// Validate before releasing the previous provider: a bad re-init must keep
|
|
38
|
+
// the working provider in place.
|
|
39
|
+
validateServiceIdentity(config);
|
|
40
|
+
this.consoleMode = normalizeConsoleLogMode(
|
|
41
|
+
config.OPENTELEMETRY_CONSOLE_LOG_MODE,
|
|
42
|
+
);
|
|
27
43
|
// Re-initialisation flushes and unregisters the previous provider first.
|
|
28
44
|
if (this.loggerProvider) {
|
|
29
45
|
await this.releaseProvider();
|
|
@@ -50,10 +66,23 @@ export class StandardLogger implements StandardLoggerInterface {
|
|
|
50
66
|
processors: [
|
|
51
67
|
new BatchLogRecordProcessor({
|
|
52
68
|
exporter,
|
|
53
|
-
maxQueueSize:
|
|
69
|
+
maxQueueSize: normalizePositiveInteger(
|
|
70
|
+
config.OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE,
|
|
71
|
+
2048,
|
|
72
|
+
),
|
|
73
|
+
maxExportBatchSize: normalizePositiveInteger(
|
|
74
|
+
config.OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE,
|
|
75
|
+
512,
|
|
76
|
+
),
|
|
77
|
+
exportTimeoutMillis: normalizePositiveInteger(
|
|
78
|
+
config.OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS,
|
|
79
|
+
30000,
|
|
80
|
+
),
|
|
54
81
|
scheduledDelayMillis:
|
|
55
|
-
(
|
|
56
|
-
|
|
82
|
+
normalizeExportIntervalSeconds(
|
|
83
|
+
config.OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS,
|
|
84
|
+
60,
|
|
85
|
+
) * 1000,
|
|
57
86
|
}),
|
|
58
87
|
],
|
|
59
88
|
resource: createOTelResource(this.serviceName, this.serviceVersion),
|
|
@@ -77,7 +106,7 @@ export class StandardLogger implements StandardLoggerInterface {
|
|
|
77
106
|
}
|
|
78
107
|
|
|
79
108
|
public createModuleLogger(moduleName: string): ModuleLogger {
|
|
80
|
-
return new ModuleLogger(moduleName, this);
|
|
109
|
+
return new ModuleLogger(moduleName, this, { consoleMode: this.consoleMode });
|
|
81
110
|
}
|
|
82
111
|
|
|
83
112
|
private registerGlobalProvider(): void {
|
|
@@ -9,6 +9,38 @@ import {
|
|
|
9
9
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
10
10
|
import { StandardMeter } from "./StandardMeter";
|
|
11
11
|
|
|
12
|
+
jest.mock("@opentelemetry/sdk-metrics", () => {
|
|
13
|
+
const actual =
|
|
14
|
+
jest.requireActual<typeof import("@opentelemetry/sdk-metrics")>(
|
|
15
|
+
"@opentelemetry/sdk-metrics",
|
|
16
|
+
);
|
|
17
|
+
const metricReaderOptions: unknown[] = [];
|
|
18
|
+
class RecordingPeriodicExportingMetricReader extends actual.PeriodicExportingMetricReader {
|
|
19
|
+
constructor(
|
|
20
|
+
options: ConstructorParameters<
|
|
21
|
+
typeof actual.PeriodicExportingMetricReader
|
|
22
|
+
>[0],
|
|
23
|
+
) {
|
|
24
|
+
super(options);
|
|
25
|
+
metricReaderOptions.push(options);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
PeriodicExportingMetricReader: RecordingPeriodicExportingMetricReader,
|
|
31
|
+
__metricReaderOptions: metricReaderOptions,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
interface SdkMetricsMock {
|
|
36
|
+
__metricReaderOptions: unknown[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function metricReaderOptions(): unknown[] {
|
|
40
|
+
return (jest.requireMock("@opentelemetry/sdk-metrics") as SdkMetricsMock)
|
|
41
|
+
.__metricReaderOptions;
|
|
42
|
+
}
|
|
43
|
+
|
|
12
44
|
jest.mock("@opentelemetry/exporter-metrics-otlp-http", () => {
|
|
13
45
|
const {
|
|
14
46
|
AggregationTemporality,
|
|
@@ -80,6 +112,7 @@ describe("StandardMeter", () => {
|
|
|
80
112
|
|
|
81
113
|
beforeEach(() => {
|
|
82
114
|
jest.clearAllMocks();
|
|
115
|
+
metricReaderOptions().length = 0;
|
|
83
116
|
metrics.disable();
|
|
84
117
|
warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
85
118
|
});
|
|
@@ -338,4 +371,76 @@ describe("StandardMeter", () => {
|
|
|
338
371
|
expect(metricNames(exporterAt(1))).toContain("after-reinit");
|
|
339
372
|
expect(shutdownSpy).toHaveBeenCalledTimes(1);
|
|
340
373
|
});
|
|
374
|
+
|
|
375
|
+
describe("export interval tuning", () => {
|
|
376
|
+
it("uses the documented default interval", async () => {
|
|
377
|
+
const meter = makeMeter(
|
|
378
|
+
meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
expect(metricReaderOptions()).toHaveLength(1);
|
|
382
|
+
expect(metricReaderOptions()[0]).toEqual({
|
|
383
|
+
exporter: expect.anything(),
|
|
384
|
+
exportIntervalMillis: 60000,
|
|
385
|
+
});
|
|
386
|
+
await meter.shutdown();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it.each([0, -5, 0.5])(
|
|
390
|
+
"clamps the export interval of %p seconds to one second",
|
|
391
|
+
async (interval) => {
|
|
392
|
+
const meter = makeMeter({
|
|
393
|
+
...meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
394
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS: interval,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(
|
|
398
|
+
(metricReaderOptions()[0] as { exportIntervalMillis: number })
|
|
399
|
+
.exportIntervalMillis,
|
|
400
|
+
).toBe(1000);
|
|
401
|
+
await meter.shutdown();
|
|
402
|
+
},
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
it("falls back to the default interval for a NaN value", async () => {
|
|
406
|
+
const meter = makeMeter({
|
|
407
|
+
...meterConfig("metric-service", "http://collector:4318/v1/metrics"),
|
|
408
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS: NaN,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
expect(
|
|
412
|
+
(metricReaderOptions()[0] as { exportIntervalMillis: number })
|
|
413
|
+
.exportIntervalMillis,
|
|
414
|
+
).toBe(60000);
|
|
415
|
+
await meter.shutdown();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("service identity validation", () => {
|
|
420
|
+
it("rejects an empty SERVICE_ID without creating an exporter", () => {
|
|
421
|
+
expect(
|
|
422
|
+
() =>
|
|
423
|
+
new StandardMeter({
|
|
424
|
+
SERVICE_ID: " ",
|
|
425
|
+
VERSION: "1.0.0",
|
|
426
|
+
OPENTELEMETRY_COLLECTOR_HTTP_METRICS:
|
|
427
|
+
"http://collector:4318/v1/metrics",
|
|
428
|
+
}),
|
|
429
|
+
).toThrow("[otel-utils] SERVICE_ID must be a non-empty string");
|
|
430
|
+
expect(mockExporterConstructor).not.toHaveBeenCalled();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("rejects an empty VERSION without creating an exporter", () => {
|
|
434
|
+
expect(
|
|
435
|
+
() =>
|
|
436
|
+
new StandardMeter({
|
|
437
|
+
SERVICE_ID: "metric-service",
|
|
438
|
+
VERSION: "",
|
|
439
|
+
OPENTELEMETRY_COLLECTOR_HTTP_METRICS:
|
|
440
|
+
"http://collector:4318/v1/metrics",
|
|
441
|
+
}),
|
|
442
|
+
).toThrow("[otel-utils] VERSION must be a non-empty string");
|
|
443
|
+
expect(mockExporterConstructor).not.toHaveBeenCalled();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
341
446
|
});
|
package/src/StandardMeter.ts
CHANGED
|
@@ -14,6 +14,10 @@ import {
|
|
|
14
14
|
PeriodicExportingMetricReader,
|
|
15
15
|
} from "@opentelemetry/sdk-metrics";
|
|
16
16
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
17
|
+
import {
|
|
18
|
+
normalizeExportIntervalSeconds,
|
|
19
|
+
validateServiceIdentity,
|
|
20
|
+
} from "./utils/configValidation";
|
|
17
21
|
|
|
18
22
|
let globalMeterProvider: MeterProvider | undefined;
|
|
19
23
|
|
|
@@ -43,6 +47,7 @@ export class StandardMeter {
|
|
|
43
47
|
private shutdownPromise?: Promise<void>;
|
|
44
48
|
|
|
45
49
|
constructor(config: ConfigOTelInterface) {
|
|
50
|
+
validateServiceIdentity(config);
|
|
46
51
|
this.serviceName = config.SERVICE_ID;
|
|
47
52
|
this.serviceVersion = config.VERSION;
|
|
48
53
|
let hasCollector = false;
|
|
@@ -64,8 +69,10 @@ export class StandardMeter {
|
|
|
64
69
|
new PeriodicExportingMetricReader({
|
|
65
70
|
exporter: metricExporter,
|
|
66
71
|
exportIntervalMillis:
|
|
67
|
-
(
|
|
68
|
-
|
|
72
|
+
normalizeExportIntervalSeconds(
|
|
73
|
+
config.OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS,
|
|
74
|
+
60,
|
|
75
|
+
) * 1000,
|
|
69
76
|
}),
|
|
70
77
|
],
|
|
71
78
|
});
|
|
@@ -225,6 +225,20 @@ describe("StandardTracer", () => {
|
|
|
225
225
|
});
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
describe("service identity validation", () => {
|
|
229
|
+
it("rejects an empty SERVICE_ID", () => {
|
|
230
|
+
expect(
|
|
231
|
+
() => new StandardTracer({ SERVICE_ID: " ", VERSION: "1.0.0" }),
|
|
232
|
+
).toThrow("[otel-utils] SERVICE_ID must be a non-empty string");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("rejects an empty VERSION", () => {
|
|
236
|
+
expect(
|
|
237
|
+
() => new StandardTracer({ SERVICE_ID: "test-service", VERSION: "" }),
|
|
238
|
+
).toThrow("[otel-utils] VERSION must be a non-empty string");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
228
242
|
describe("provider ownership and global registration", () => {
|
|
229
243
|
it("routes each instance to its own exporter and resource", async () => {
|
|
230
244
|
const first = makeTracer(
|
package/src/StandardTracer.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
ATTR_HTTP_ROUTE,
|
|
22
22
|
} from "@opentelemetry/semantic-conventions";
|
|
23
23
|
import { ConfigOTelInterface } from "./models/ConfigOTelInterface";
|
|
24
|
+
import { validateServiceIdentity } from "./utils/configValidation";
|
|
24
25
|
import { createOTelResource } from "./utils/createResource";
|
|
25
26
|
|
|
26
27
|
let globalTracerProvider: TracerProvider | undefined;
|
|
@@ -64,6 +65,7 @@ export class StandardTracer {
|
|
|
64
65
|
private shutdownPromise?: Promise<void>;
|
|
65
66
|
|
|
66
67
|
constructor(config: ConfigOTelInterface) {
|
|
68
|
+
validateServiceIdentity(config);
|
|
67
69
|
this.serviceName = config.SERVICE_ID;
|
|
68
70
|
this.serviceVersion = config.VERSION;
|
|
69
71
|
const spanProcessors = [];
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type ConsoleLogMode = "always" | "fallback" | "never";
|
|
2
|
+
|
|
1
3
|
export interface ConfigOTelInterface {
|
|
2
4
|
SERVICE_ID: string;
|
|
3
5
|
VERSION: string;
|
|
@@ -6,5 +8,9 @@ export interface ConfigOTelInterface {
|
|
|
6
8
|
OPENTELEMETRY_COLLECTOR_HTTP_LOGS?: string;
|
|
7
9
|
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS?: number;
|
|
8
10
|
OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS?: number;
|
|
11
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE?: number;
|
|
12
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE?: number;
|
|
13
|
+
OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS?: number;
|
|
14
|
+
OPENTELEMETRY_CONSOLE_LOG_MODE?: ConsoleLogMode;
|
|
9
15
|
OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER?: string;
|
|
10
16
|
}
|