@devopsplaybook.io/otel-utils 1.8.0-beta.39.883f25d → 1.9.0-beta.40.3ebb823
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 +36 -29
- package/dist/src/StandardLogger.js +3 -1
- package/dist/src/StandardTracer.d.ts +8 -2
- package/dist/src/StandardTracer.js +7 -4
- package/package.json +18 -5
- package/.github/workflows/main-build.yml +0 -16
- package/.github/workflows/pr-check.yml +0 -25
- package/index.ts +0 -7
- package/jest.config.js +0 -17
- package/prettierrc.json +0 -5
- package/src/ModuleLogger.spec.ts +0 -253
- package/src/ModuleLogger.ts +0 -137
- package/src/ShutdownHooks.spec.ts +0 -75
- package/src/ShutdownHooks.ts +0 -45
- package/src/StandardLogger.spec.ts +0 -479
- package/src/StandardLogger.ts +0 -149
- package/src/StandardMeter.spec.ts +0 -446
- package/src/StandardMeter.ts +0 -167
- package/src/StandardTracer.spec.ts +0 -349
- package/src/StandardTracer.ts +0 -190
- package/src/createResource.spec.ts +0 -20
- package/src/models/ConfigOTelInterface.ts +0 -16
- package/src/models/StandardLoggerInterface.ts +0 -5
- package/src/utils/configValidation.spec.ts +0 -141
- package/src/utils/configValidation.ts +0 -50
- package/src/utils/createResource.ts +0 -20
- package/tsconfig.json +0 -14
- package/tsconfig.spec.json +0 -8
package/README.md
CHANGED
|
@@ -8,24 +8,26 @@ Utility library that simplifies OpenTelemetry integration for Node.js services.
|
|
|
8
8
|
npm install @devopsplaybook.io/otel-utils
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Requires Node.js 22 or newer (`engines` in `package.json`).
|
|
12
|
+
|
|
11
13
|
## Configuration
|
|
12
14
|
|
|
13
15
|
All classes accept a `ConfigOTelInterface` object. The fields mirror environment variables in the consuming application (for example `common-utils`' `ConfigBase`); the library itself does not read `process.env`.
|
|
14
16
|
|
|
15
|
-
| Field | Type | Default | Description
|
|
16
|
-
| --------------------------------------------------------- | ----------------------------------- | ---------- |
|
|
17
|
-
| `SERVICE_ID` | `string` | — | Service identifier (required, non-empty)
|
|
18
|
-
| `VERSION` | `string` | — | Service version (required, non-empty)
|
|
19
|
-
| `OPENTELEMETRY_COLLECTOR_HTTP_TRACES` | `string?` | — | OTLP HTTP endpoint for traces
|
|
20
|
-
| `OPENTELEMETRY_COLLECTOR_HTTP_METRICS` | `string?` | — | OTLP HTTP endpoint for metrics
|
|
21
|
-
| `OPENTELEMETRY_COLLECTOR_HTTP_LOGS` | `string?` | — | OTLP HTTP endpoint for logs
|
|
22
|
-
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS` | `number?` | `60` | Log export interval in seconds (clamped to ≥ 1)
|
|
17
|
+
| Field | Type | Default | Description |
|
|
18
|
+
| --------------------------------------------------------- | ----------------------------------- | ---------- | --------------------------------------------------- |
|
|
19
|
+
| `SERVICE_ID` | `string` | — | Service identifier (required, non-empty) |
|
|
20
|
+
| `VERSION` | `string` | — | Service version (required, non-empty) |
|
|
21
|
+
| `OPENTELEMETRY_COLLECTOR_HTTP_TRACES` | `string?` | — | OTLP HTTP endpoint for traces |
|
|
22
|
+
| `OPENTELEMETRY_COLLECTOR_HTTP_METRICS` | `string?` | — | OTLP HTTP endpoint for metrics |
|
|
23
|
+
| `OPENTELEMETRY_COLLECTOR_HTTP_LOGS` | `string?` | — | OTLP HTTP endpoint for logs |
|
|
24
|
+
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS` | `number?` | `60` | Log export interval in seconds (clamped to ≥ 1) |
|
|
23
25
|
| `OPENTELEMETRY_COLLECTOR_EXPORT_METRICS_INTERVAL_SECONDS` | `number?` | `60` | Metrics export interval in seconds (clamped to ≥ 1) |
|
|
24
|
-
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE` | `number?` | `2048` | Max log records buffered before export
|
|
25
|
-
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE` | `number?` | `512` | Max log records per export batch
|
|
26
|
-
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS` | `number?` | `30000` | Log export timeout in milliseconds
|
|
27
|
-
| `OPENTELEMETRY_CONSOLE_LOG_MODE` | `"always" \| "fallback" \| "never"` | `"always"` | Console mirroring mode for `ModuleLogger`
|
|
28
|
-
| `OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER` | `string?` | — | Bearer token for collector auth
|
|
26
|
+
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE` | `number?` | `2048` | Max log records buffered before export |
|
|
27
|
+
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE` | `number?` | `512` | Max log records per export batch |
|
|
28
|
+
| `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS` | `number?` | `30000` | Log export timeout in milliseconds |
|
|
29
|
+
| `OPENTELEMETRY_CONSOLE_LOG_MODE` | `"always" \| "fallback" \| "never"` | `"always"` | Console mirroring mode for `ModuleLogger` |
|
|
30
|
+
| `OPENTELEMETRY_COLLECT_AUTHORIZATION_HEADER` | `string?` | — | Bearer token for collector auth |
|
|
29
31
|
|
|
30
32
|
`SERVICE_ID` and `VERSION` must be non-empty strings. `StandardTracer`/`StandardMeter` constructors and `StandardLogger.initOTel` fail fast with `[otel-utils] SERVICE_ID must be a non-empty string` / `[otel-utils] VERSION must be a non-empty string` otherwise. A failed re-initialisation of `StandardLogger` keeps the previous provider in place.
|
|
31
33
|
|
|
@@ -57,8 +59,8 @@ span.end();
|
|
|
57
59
|
|
|
58
60
|
**Methods:**
|
|
59
61
|
|
|
60
|
-
- `startSpan(name, parentSpan?, options?)` — Creates a span
|
|
61
|
-
- `static updateHttpHeader(context, headers?)` — Injects W3C trace context into an HTTP headers object for propagation to downstream services.
|
|
62
|
+
- `startSpan(name, parentSpan?, options?)` — Creates a span and returns the SDK `Span` from `@opentelemetry/sdk-trace`. Span names are sanitized to `[a-zA-Z0-9-_/]`. `parentSpan` accepts any API `Span` (for example from `trace.getSpan(context)` or `trace.wrapSpanContext(...)`), so callers need no cast. When neither `parentSpan` nor `options` is given, the span is `SpanKind.INTERNAL` and carries `http.request.method=BACKEND` and a synthetic `http.route` attribute with the value `${serviceName}-${version}-${sanitizedSpanName}`. Passing a `parentSpan`, an `options` object (e.g. `{ kind: SpanKind.SERVER }`), or both forwards them to the OpenTelemetry SDK instead and skips those synthetic attributes, because the caller then describes the span itself. Generic HTTP dashboards that group by `http.request.method`/`http.route` count these spans as `BACKEND` requests with a synthetic route; this is intentional and consumed by otel-light.
|
|
63
|
+
- `static updateHttpHeader<T extends object = Record<string, string>>(context, headers?)` — Injects the W3C trace context of `context` into an HTTP headers object for propagation to downstream services, and returns that same object (a new one when omitted). Only `traceparent` and `tracestate` are injected: the propagation context is built from the span alone, so baggage and other active-context values are **not** propagated and an existing `baggage` header is left untouched.
|
|
62
64
|
- `shutdown()` — Flushes buffered spans and shuts the tracer provider down. Idempotent.
|
|
63
65
|
|
|
64
66
|
### StandardLogger
|
|
@@ -86,12 +88,12 @@ moduleLog.info("Hello world");
|
|
|
86
88
|
|
|
87
89
|
`BatchLogRecordProcessor` buffers records in memory and exports them periodically:
|
|
88
90
|
|
|
89
|
-
| Option
|
|
90
|
-
|
|
|
91
|
-
| `scheduledDelayMillis` | `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS` | 60000 ms
|
|
92
|
-
| `maxQueueSize`
|
|
93
|
-
| `maxExportBatchSize`
|
|
94
|
-
| `exportTimeoutMillis`
|
|
91
|
+
| Option | Config field | Default |
|
|
92
|
+
| ---------------------- | ------------------------------------------------------ | -------- |
|
|
93
|
+
| `scheduledDelayMillis` | `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_INTERVAL_SECONDS` | 60000 ms |
|
|
94
|
+
| `maxQueueSize` | `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_QUEUE_SIZE` | 2048 |
|
|
95
|
+
| `maxExportBatchSize` | `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_MAX_BATCH_SIZE` | 512 |
|
|
96
|
+
| `exportTimeoutMillis` | `OPENTELEMETRY_COLLECTOR_EXPORT_LOGS_TIMEOUT_MILLIS` | 30000 ms |
|
|
95
97
|
|
|
96
98
|
A record waits up to `scheduledDelayMillis` (60 s by default) before it is exported; each export sends at most `maxExportBatchSize` (512) records with a 30 s timeout, and at most `maxQueueSize` (2048) records are buffered. Records that arrive while the queue is full are **dropped silently** — no warning or fallback is emitted. A crash or restart loses every record buffered since the last export; call `shutdown()` or use `registerShutdownHooks` (see [Graceful shutdown](#graceful-shutdown)) to flush them. Keep `maxExportBatchSize` ≤ `maxQueueSize` (the SDK clamps it otherwise).
|
|
97
99
|
|
|
@@ -109,11 +111,11 @@ await logger.initOTel({
|
|
|
109
111
|
|
|
110
112
|
Scoped logger that prefixes all output with a module name. Every record carries a `log.module` attribute in addition to the `[module] message` body. Records are emitted to the OTel logger when one is available, and can additionally be mirrored to the console, controlled by `OPENTELEMETRY_CONSOLE_LOG_MODE`:
|
|
111
113
|
|
|
112
|
-
| Mode
|
|
113
|
-
|
|
|
114
|
-
| `always` (default)
|
|
115
|
-
| `fallback`
|
|
116
|
-
| `never`
|
|
114
|
+
| Mode | Behaviour |
|
|
115
|
+
| ------------------ | ------------------------------------------------------------------------------------------ |
|
|
116
|
+
| `always` (default) | Mirror every record to the console, whether or not OTel logging is configured. |
|
|
117
|
+
| `fallback` | Mirror only when no OTel logger is configured (`OPENTELEMETRY_COLLECTOR_HTTP_LOGS` unset). |
|
|
118
|
+
| `never` | Never write to the console; records only go to OTel. |
|
|
117
119
|
|
|
118
120
|
Mirroring is level-aware: `info` uses `console.log`, `warn` uses `console.warn` and `error` uses `console.error` (stderr), so stderr-based alerting sees warnings and errors. An invalid mode value logs one `console.warn` and falls back to `always`. With `never`, a `ModuleLogger` writes nothing to the console even when no OTel logger is configured.
|
|
119
121
|
|
|
@@ -272,8 +274,8 @@ All three signals share the same service identity (name + version) and resource
|
|
|
272
274
|
|
|
273
275
|
## Dependencies
|
|
274
276
|
|
|
275
|
-
- `@opentelemetry/api`
|
|
276
|
-
- `@opentelemetry/sdk-trace
|
|
277
|
+
- `@opentelemetry/api` — OpenTelemetry API interfaces
|
|
278
|
+
- `@opentelemetry/sdk-trace` — Tracing SDK
|
|
277
279
|
- `@opentelemetry/sdk-metrics` — Metrics SDK
|
|
278
280
|
- `@opentelemetry/sdk-logs` — Logging SDK
|
|
279
281
|
- `@opentelemetry/exporter-trace-otlp-http` / `@opentelemetry/exporter-metrics-otlp-http` / `@opentelemetry/exporter-logs-otlp-http` — OTLP HTTP exporters
|
|
@@ -282,8 +284,13 @@ All three signals share the same service identity (name + version) and resource
|
|
|
282
284
|
- `@opentelemetry/core` — W3C trace context propagation
|
|
283
285
|
- `@opentelemetry/resources` / `@opentelemetry/semantic-conventions` — Resource attributes
|
|
284
286
|
|
|
287
|
+
Dev-only `npm audit` advisories (such as those previously reported in the Jest toolchain via `js-yaml`/`sprintf-js`) are tracked by the regular dependency-update task, not addressed here. The current lockfile reports no vulnerabilities, with or without dev dependencies.
|
|
288
|
+
|
|
285
289
|
## Build
|
|
286
290
|
|
|
287
291
|
```bash
|
|
288
|
-
npm
|
|
292
|
+
npm ci # install the locked dependencies
|
|
293
|
+
npm run build # tsc → dist/, then type-check the specs
|
|
294
|
+
npm run lint # oxlint + Prettier check
|
|
295
|
+
npm test # Jest unit tests with coverage
|
|
289
296
|
```
|
|
@@ -63,7 +63,9 @@ class StandardLogger {
|
|
|
63
63
|
return this.logger;
|
|
64
64
|
}
|
|
65
65
|
createModuleLogger(moduleName) {
|
|
66
|
-
return new ModuleLogger_1.ModuleLogger(moduleName, this, {
|
|
66
|
+
return new ModuleLogger_1.ModuleLogger(moduleName, this, {
|
|
67
|
+
consoleMode: this.consoleMode,
|
|
68
|
+
});
|
|
67
69
|
}
|
|
68
70
|
registerGlobalProvider() {
|
|
69
71
|
if (globalLoggerProvider) {
|
|
@@ -26,8 +26,14 @@ export declare class StandardTracer {
|
|
|
26
26
|
private shutdownPromise?;
|
|
27
27
|
constructor(config: ConfigOTelInterface);
|
|
28
28
|
shutdown(): Promise<void>;
|
|
29
|
-
startSpan(name: string, parentSpan?:
|
|
30
|
-
|
|
29
|
+
startSpan(name: string, parentSpan?: ApiSpan, options?: StandardTracerStartSpanOptions): Span;
|
|
30
|
+
/**
|
|
31
|
+
* Injects the W3C trace context of `context` into `headers` and returns it.
|
|
32
|
+
*
|
|
33
|
+
* The propagation context is built from the span alone (ROOT_CONTEXT), so
|
|
34
|
+
* only traceparent/tracestate are injected; baggage is not propagated.
|
|
35
|
+
*/
|
|
36
|
+
static updateHttpHeader<T extends object = Record<string, string>>(context: ApiSpan, headers?: T): T;
|
|
31
37
|
private registerGlobalProvider;
|
|
32
38
|
private doShutdown;
|
|
33
39
|
}
|
|
@@ -68,11 +68,14 @@ class StandardTracer {
|
|
|
68
68
|
span.setAttribute(semantic_conventions_1.ATTR_HTTP_ROUTE, `${this.serviceName}-${this.serviceVersion}-${sanitizedName}`);
|
|
69
69
|
return span;
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Injects the W3C trace context of `context` into `headers` and returns it.
|
|
73
|
+
*
|
|
74
|
+
* The propagation context is built from the span alone (ROOT_CONTEXT), so
|
|
75
|
+
* only traceparent/tracestate are injected; baggage is not propagated.
|
|
76
|
+
*/
|
|
72
77
|
static updateHttpHeader(context, headers = {}) {
|
|
73
|
-
StandardTracer.PROPAGATOR.inject(api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, context.spanContext()),
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
-
headers, api_1.defaultTextMapSetter);
|
|
78
|
+
StandardTracer.PROPAGATOR.inject(api_1.trace.setSpanContext(api_1.ROOT_CONTEXT, context.spanContext()), headers, api_1.defaultTextMapSetter);
|
|
76
79
|
return headers;
|
|
77
80
|
}
|
|
78
81
|
registerGlobalProvider() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devopsplaybook.io/otel-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0-beta.40.3ebb823",
|
|
4
4
|
"description": "Utility to simplify integration with Open Telemetry",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Open",
|
|
@@ -14,11 +14,24 @@
|
|
|
14
14
|
"type": "commonjs",
|
|
15
15
|
"main": "dist/index.js",
|
|
16
16
|
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
17
30
|
"scripts": {
|
|
18
31
|
"build": "tsc && tsc -p tsconfig.spec.json --noEmit",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
32
|
+
"format": "prettier --write .",
|
|
33
|
+
"lint": "oxlint index.ts src && prettier --check .",
|
|
34
|
+
"test": "jest --coverage"
|
|
22
35
|
},
|
|
23
36
|
"dependencies": {
|
|
24
37
|
"@opentelemetry/api": "^1.10.0",
|
|
@@ -39,9 +52,9 @@
|
|
|
39
52
|
"@swc/jest": "^0.2.39",
|
|
40
53
|
"@types/jest": "^30.0.0",
|
|
41
54
|
"@types/node": "^26.6.5",
|
|
42
|
-
"@types/sqlite3": "^5.1.0",
|
|
43
55
|
"jest": "^30.5.2",
|
|
44
56
|
"oxlint": "^1.89.0",
|
|
57
|
+
"prettier": "^3.9.10",
|
|
45
58
|
"typescript": "^7.0.2"
|
|
46
59
|
},
|
|
47
60
|
"publishConfig": {
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
name: Main Build
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: ["main"]
|
|
6
|
-
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
npm-merge:
|
|
11
|
-
uses: devopsplaybook-io/common-utils/.github/workflows/reusable-npm-merge.yml@main
|
|
12
|
-
with:
|
|
13
|
-
npm_package_name: "@devopsplaybook.io/otel-utils"
|
|
14
|
-
node_version: "22"
|
|
15
|
-
secrets:
|
|
16
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
name: PR Check
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: ["main"]
|
|
6
|
-
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
concurrency:
|
|
10
|
-
group: pr-${{ github.event.pull_request.number || github.ref }}
|
|
11
|
-
cancel-in-progress: true
|
|
12
|
-
|
|
13
|
-
permissions:
|
|
14
|
-
contents: read
|
|
15
|
-
pull-requests: write
|
|
16
|
-
issues: write
|
|
17
|
-
|
|
18
|
-
jobs:
|
|
19
|
-
npm-pr:
|
|
20
|
-
uses: devopsplaybook-io/common-utils/.github/workflows/reusable-npm-pr.yml@main
|
|
21
|
-
with:
|
|
22
|
-
npm_package_name: "@devopsplaybook.io/otel-utils"
|
|
23
|
-
node_version: "22"
|
|
24
|
-
secrets:
|
|
25
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/index.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export * from "./src/models/ConfigOTelInterface";
|
|
2
|
-
export * from "./src/models/StandardLoggerInterface";
|
|
3
|
-
export * from "./src/ModuleLogger";
|
|
4
|
-
export * from "./src/ShutdownHooks";
|
|
5
|
-
export * from "./src/StandardLogger";
|
|
6
|
-
export * from "./src/StandardMeter";
|
|
7
|
-
export * from "./src/StandardTracer";
|
package/jest.config.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
moduleFileExtensions: ["ts", "js"],
|
|
3
|
-
transform: {
|
|
4
|
-
"^.+\\.(ts|tsx)$": [
|
|
5
|
-
"@swc/jest",
|
|
6
|
-
{
|
|
7
|
-
jsc: {
|
|
8
|
-
target: "es2019",
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
],
|
|
12
|
-
},
|
|
13
|
-
coverageProvider: "v8",
|
|
14
|
-
testMatch: ["/**/src/**/*.spec.(ts|js)"],
|
|
15
|
-
testPathIgnorePatterns: ["/node_modules/", "/dist/"],
|
|
16
|
-
testEnvironment: "node",
|
|
17
|
-
};
|
package/prettierrc.json
DELETED
package/src/ModuleLogger.spec.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import type { Span } from "@opentelemetry/api";
|
|
2
|
-
import {
|
|
3
|
-
SeverityNumber,
|
|
4
|
-
type Logger as OTelLogger,
|
|
5
|
-
} from "@opentelemetry/api";
|
|
6
|
-
import { ModuleLogger, type ModuleLoggerOptions } from "./ModuleLogger";
|
|
7
|
-
import type { StandardLoggerInterface } from "./models/StandardLoggerInterface";
|
|
8
|
-
|
|
9
|
-
describe("ModuleLogger", () => {
|
|
10
|
-
let logSpy: jest.SpyInstance;
|
|
11
|
-
let warnSpy: jest.SpyInstance;
|
|
12
|
-
let errorSpy: jest.SpyInstance;
|
|
13
|
-
let emit: jest.Mock;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
emit = jest.fn();
|
|
17
|
-
logSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
|
18
|
-
warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
19
|
-
errorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
logSpy.mockRestore();
|
|
24
|
-
warnSpy.mockRestore();
|
|
25
|
-
errorSpy.mockRestore();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
function createLogger(
|
|
29
|
-
getLogger: () => OTelLogger | undefined,
|
|
30
|
-
): StandardLoggerInterface {
|
|
31
|
-
return { getLogger };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function loggerWithEmit(options?: ModuleLoggerOptions): ModuleLogger {
|
|
35
|
-
const fakeLogger = { emit } as unknown as OTelLogger;
|
|
36
|
-
return new ModuleLogger("billing", createLogger(() => fakeLogger), options);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function lastAttributes(): Record<string, unknown> {
|
|
40
|
-
const record = emit.mock.calls[emit.mock.calls.length - 1][0] as {
|
|
41
|
-
attributes: Record<string, unknown>;
|
|
42
|
-
};
|
|
43
|
-
return record.attributes;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function expectOnlyDefinedAttributes(): void {
|
|
47
|
-
for (const [key, value] of Object.entries(lastAttributes())) {
|
|
48
|
-
expect({ key, defined: value !== undefined }).toEqual({
|
|
49
|
-
key,
|
|
50
|
-
defined: true,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
it("logs info to console.log and the OTel logger with the module attribute", () => {
|
|
56
|
-
loggerWithEmit().info("hello");
|
|
57
|
-
|
|
58
|
-
expect(logSpy).toHaveBeenCalledWith("[info] [billing] hello");
|
|
59
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
60
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
61
|
-
expect(emit).toHaveBeenCalledWith({
|
|
62
|
-
severityNumber: SeverityNumber.INFO,
|
|
63
|
-
severityText: "info",
|
|
64
|
-
body: "[billing] hello",
|
|
65
|
-
attributes: { "log.type": "custom", "log.module": "billing" },
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("mirrors warnings through console.warn", () => {
|
|
70
|
-
loggerWithEmit().warn("careful");
|
|
71
|
-
|
|
72
|
-
expect(warnSpy).toHaveBeenCalledWith("[warn] [billing] careful");
|
|
73
|
-
expect(logSpy).not.toHaveBeenCalled();
|
|
74
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
75
|
-
expect(emit).toHaveBeenCalledWith(
|
|
76
|
-
expect.objectContaining({
|
|
77
|
-
severityNumber: SeverityNumber.WARN,
|
|
78
|
-
severityText: "warn",
|
|
79
|
-
body: "[billing] careful",
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("mirrors errors through console.error", () => {
|
|
85
|
-
loggerWithEmit().error("failed");
|
|
86
|
-
|
|
87
|
-
expect(errorSpy).toHaveBeenCalledWith("[error] [billing] failed");
|
|
88
|
-
expect(logSpy).not.toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("logs errors without duplicating the stack in the record body", () => {
|
|
92
|
-
const error = new Error("boom");
|
|
93
|
-
loggerWithEmit().error("request failed", error);
|
|
94
|
-
|
|
95
|
-
expect(emit).toHaveBeenCalledWith({
|
|
96
|
-
severityNumber: SeverityNumber.ERROR,
|
|
97
|
-
severityText: "error",
|
|
98
|
-
body: "[billing] request failed",
|
|
99
|
-
attributes: {
|
|
100
|
-
"log.type": "custom",
|
|
101
|
-
"log.module": "billing",
|
|
102
|
-
"exception.type": "Error",
|
|
103
|
-
"exception.message": "boom",
|
|
104
|
-
"exception.stacktrace": error.stack,
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("normalises a string error instead of losing its message", () => {
|
|
110
|
-
loggerWithEmit().error("failed", "string-error");
|
|
111
|
-
|
|
112
|
-
const attributes = lastAttributes();
|
|
113
|
-
expect(attributes["exception.type"]).toBe("Error");
|
|
114
|
-
expect(attributes["exception.message"]).toBe("string-error");
|
|
115
|
-
expect(typeof attributes["exception.stacktrace"]).toBe("string");
|
|
116
|
-
expectOnlyDefinedAttributes();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("serialises a plain object error", () => {
|
|
120
|
-
loggerWithEmit().error("failed", { code: 42 });
|
|
121
|
-
|
|
122
|
-
const attributes = lastAttributes();
|
|
123
|
-
expect(attributes["exception.type"]).toBe("Error");
|
|
124
|
-
expect(attributes["exception.message"]).toBe('{"code":42}');
|
|
125
|
-
expect(typeof attributes["exception.stacktrace"]).toBe("string");
|
|
126
|
-
expectOnlyDefinedAttributes();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it("adds no exception attributes without an error", () => {
|
|
130
|
-
loggerWithEmit().error("failed");
|
|
131
|
-
expect(lastAttributes()).toEqual({
|
|
132
|
-
"log.type": "custom",
|
|
133
|
-
"log.module": "billing",
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
loggerWithEmit().error("failed", null);
|
|
137
|
-
expect(lastAttributes()).toEqual({
|
|
138
|
-
"log.type": "custom",
|
|
139
|
-
"log.module": "billing",
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("omits exception.stacktrace when the error has no stack", () => {
|
|
144
|
-
const error = new Error("no stack");
|
|
145
|
-
delete error.stack;
|
|
146
|
-
|
|
147
|
-
loggerWithEmit().error("failed", error);
|
|
148
|
-
|
|
149
|
-
const attributes = lastAttributes();
|
|
150
|
-
expect(attributes["exception.type"]).toBe("Error");
|
|
151
|
-
expect(attributes["exception.message"]).toBe("no stack");
|
|
152
|
-
expect(attributes).not.toHaveProperty("exception.stacktrace");
|
|
153
|
-
expectOnlyDefinedAttributes();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("treats a span passed as the second argument as context", () => {
|
|
157
|
-
const span = {
|
|
158
|
-
spanContext: () => ({ spanId: "span-1", traceId: "trace-1" }),
|
|
159
|
-
} as unknown as Span;
|
|
160
|
-
|
|
161
|
-
loggerWithEmit().error("failed", span);
|
|
162
|
-
|
|
163
|
-
expect(errorSpy).toHaveBeenCalledWith("[error] [billing] failed");
|
|
164
|
-
expect(lastAttributes()).toEqual({
|
|
165
|
-
"log.type": "custom",
|
|
166
|
-
"log.module": "billing",
|
|
167
|
-
"span.id": "span-1",
|
|
168
|
-
"trace.id": "trace-1",
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("adds span and trace ids when a span context is provided", () => {
|
|
173
|
-
const span = {
|
|
174
|
-
spanContext: () => ({ spanId: "span-1", traceId: "trace-1" }),
|
|
175
|
-
} as unknown as Span;
|
|
176
|
-
|
|
177
|
-
loggerWithEmit().info("inside a span", span);
|
|
178
|
-
|
|
179
|
-
expect(emit).toHaveBeenCalledWith(
|
|
180
|
-
expect.objectContaining({
|
|
181
|
-
attributes: expect.objectContaining({
|
|
182
|
-
"span.id": "span-1",
|
|
183
|
-
"trace.id": "trace-1",
|
|
184
|
-
}),
|
|
185
|
-
}),
|
|
186
|
-
);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("combines exception attributes with a span context", () => {
|
|
190
|
-
const span = {
|
|
191
|
-
spanContext: () => ({ spanId: "span-2", traceId: "trace-2" }),
|
|
192
|
-
} as unknown as Span;
|
|
193
|
-
|
|
194
|
-
loggerWithEmit().error("failed", new Error("boom"), span);
|
|
195
|
-
|
|
196
|
-
const attributes = lastAttributes();
|
|
197
|
-
expect(attributes["exception.message"]).toBe("boom");
|
|
198
|
-
expect(attributes["span.id"]).toBe("span-2");
|
|
199
|
-
expect(attributes["trace.id"]).toBe("trace-2");
|
|
200
|
-
expectOnlyDefinedAttributes();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe("console modes", () => {
|
|
204
|
-
it("always mirrors with and without an OTel logger", () => {
|
|
205
|
-
loggerWithEmit({ consoleMode: "always" }).info("with logger");
|
|
206
|
-
expect(logSpy).toHaveBeenCalledWith("[info] [billing] with logger");
|
|
207
|
-
expect(emit).toHaveBeenCalledTimes(1);
|
|
208
|
-
|
|
209
|
-
const withoutLogger = new ModuleLogger(
|
|
210
|
-
"billing",
|
|
211
|
-
createLogger(() => undefined),
|
|
212
|
-
{ consoleMode: "always" },
|
|
213
|
-
);
|
|
214
|
-
withoutLogger.info("without logger");
|
|
215
|
-
expect(logSpy).toHaveBeenCalledWith(
|
|
216
|
-
"[info] [billing] without logger",
|
|
217
|
-
);
|
|
218
|
-
expect(emit).toHaveBeenCalledTimes(1);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
it("fallback mirrors only when no OTel logger is available", () => {
|
|
222
|
-
loggerWithEmit({ consoleMode: "fallback" }).info("with logger");
|
|
223
|
-
expect(logSpy).not.toHaveBeenCalled();
|
|
224
|
-
expect(emit).toHaveBeenCalledTimes(1);
|
|
225
|
-
|
|
226
|
-
const withoutLogger = new ModuleLogger(
|
|
227
|
-
"billing",
|
|
228
|
-
createLogger(() => undefined),
|
|
229
|
-
{ consoleMode: "fallback" },
|
|
230
|
-
);
|
|
231
|
-
withoutLogger.warn("without logger");
|
|
232
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
233
|
-
"[warn] [billing] without logger",
|
|
234
|
-
);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("never disables console output while records are still emitted", () => {
|
|
238
|
-
loggerWithEmit({ consoleMode: "never" }).error("silent");
|
|
239
|
-
|
|
240
|
-
expect(logSpy).not.toHaveBeenCalled();
|
|
241
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
242
|
-
expect(errorSpy).not.toHaveBeenCalled();
|
|
243
|
-
expect(emit).toHaveBeenCalledWith(
|
|
244
|
-
expect.objectContaining({ severityText: "error" }),
|
|
245
|
-
);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("defaults to always when no options are given", () => {
|
|
249
|
-
new ModuleLogger("billing", createLogger(() => undefined)).info("legacy");
|
|
250
|
-
expect(logSpy).toHaveBeenCalledWith("[info] [billing] legacy");
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
});
|