@contrail/telemetry 2.0.0-dev-telemetry-own-otel-1 → 2.0.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/CHANGELOG.md +15 -0
- package/lib/logger/index.d.ts +2 -1
- package/lib/logger/index.js +10 -4
- package/lib/logger/log-context.d.ts +61 -0
- package/lib/logger/log-context.js +80 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.0.0] - 2026-04-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Request context on every log** — `logger` is now context-aware. During a request, all logs (including from library code like `@contrail/dynamodb` or `@contrail/auth`) automatically include `user.id`, `org.slug`, `url.path`, and other request fields. No import changes needed.
|
|
15
|
+
- **`withLogContext(fields, fn)`** — Wrap any block of code with extra log context. Context stacks on top of any existing request context and is removed when the callback exits.
|
|
16
|
+
- **`addStream(streamEntry)`** — Hook a custom pino stream into the logger to tap into all log records (e.g. collect logs in memory for a UI panel).
|
|
17
|
+
- **`flushLogs()`** — Flush pending logs to the OTLP collector before a Lambda execution environment freezes.
|
|
18
|
+
- **OTel log export** — Logs are sent to an OTLP HTTP endpoint (when `OTEL_EXPORTER_OTLP_ENDPOINT` is set) with Lambda attributes, span context, and flattened structured fields.
|
|
19
|
+
- **Structured stdout** — CloudWatch-friendly tab-delimited format with `pino-pretty` support in local dev.
|
|
20
|
+
|
|
21
|
+
### Breaking Changes
|
|
22
|
+
|
|
23
|
+
- **`logger` is now a Proxy, not a raw Pino instance.** Behavior is identical when no request context is active (falls back to the base logger). This only matters if you were comparing `logger === baseLogger` — they are no longer the same reference. Use `baseLogger` if you need the raw Pino instance.
|
|
24
|
+
|
|
10
25
|
## [1.0.2] - 2026-03-25
|
|
11
26
|
|
|
12
27
|
### Added
|
package/lib/logger/index.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ import pino from 'pino';
|
|
|
2
2
|
export { parseOtelResourceAttributes } from './parse-otel-resource-attributes';
|
|
3
3
|
export { PINO_LEVEL_TO_OTEL_SEVERITY, PINO_LEVEL_TO_NAME } from './logger-config';
|
|
4
4
|
export { ATTR_LOG_MESSAGE, ATTR_LOG_PAYLOAD } from './semantic-conventions';
|
|
5
|
+
export { loggerStorage, withLogContext } from './log-context';
|
|
5
6
|
export declare const baseLogger: pino.Logger<never, boolean>;
|
|
6
|
-
export declare const logger: pino.Logger
|
|
7
|
+
export declare const logger: pino.Logger;
|
|
7
8
|
/**
|
|
8
9
|
* Add a stream destination to the logger's multistream.
|
|
9
10
|
* Use this to wire additional log sinks (e.g. InMemoryLogStream in app-framework).
|
package/lib/logger/index.js
CHANGED
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.logger = exports.baseLogger = exports.ATTR_LOG_PAYLOAD = exports.ATTR_LOG_MESSAGE = exports.PINO_LEVEL_TO_NAME = exports.PINO_LEVEL_TO_OTEL_SEVERITY = exports.parseOtelResourceAttributes = void 0;
|
|
7
|
+
exports.logger = exports.baseLogger = exports.withLogContext = exports.loggerStorage = exports.ATTR_LOG_PAYLOAD = exports.ATTR_LOG_MESSAGE = exports.PINO_LEVEL_TO_NAME = exports.PINO_LEVEL_TO_OTEL_SEVERITY = exports.parseOtelResourceAttributes = void 0;
|
|
8
8
|
exports.addStream = addStream;
|
|
9
9
|
exports.flushLogs = flushLogs;
|
|
10
10
|
const pino_1 = __importDefault(require("pino"));
|
|
@@ -29,6 +29,9 @@ Object.defineProperty(exports, "PINO_LEVEL_TO_NAME", { enumerable: true, get: fu
|
|
|
29
29
|
var semantic_conventions_3 = require("./semantic-conventions");
|
|
30
30
|
Object.defineProperty(exports, "ATTR_LOG_MESSAGE", { enumerable: true, get: function () { return semantic_conventions_3.ATTR_LOG_MESSAGE; } });
|
|
31
31
|
Object.defineProperty(exports, "ATTR_LOG_PAYLOAD", { enumerable: true, get: function () { return semantic_conventions_3.ATTR_LOG_PAYLOAD; } });
|
|
32
|
+
var log_context_1 = require("./log-context");
|
|
33
|
+
Object.defineProperty(exports, "loggerStorage", { enumerable: true, get: function () { return log_context_1.loggerStorage; } });
|
|
34
|
+
Object.defineProperty(exports, "withLogContext", { enumerable: true, get: function () { return log_context_1.withLogContext; } });
|
|
32
35
|
// --- Environment & Resource Setup ---
|
|
33
36
|
const semantic_conventions_4 = require("../semantic-conventions");
|
|
34
37
|
const LAMBDA_ENV_OTEL_ATTRIBUTES = getLambdaEnvAttributes();
|
|
@@ -230,7 +233,8 @@ exports.baseLogger = (0, pino_1.default)({
|
|
|
230
233
|
[semantic_conventions_1.ATTR_SERVICE_VERSION]: serviceVersion,
|
|
231
234
|
},
|
|
232
235
|
}, pino_1.default.multistream(buildPinoStreams()));
|
|
233
|
-
|
|
236
|
+
const log_context_2 = require("./log-context");
|
|
237
|
+
exports.logger = (0, log_context_2.createContextAwareLogger)(exports.baseLogger);
|
|
234
238
|
// --- Public API ---
|
|
235
239
|
/**
|
|
236
240
|
* Add a stream destination to the logger's multistream.
|
|
@@ -257,10 +261,12 @@ async function flushLogs(options) {
|
|
|
257
261
|
]);
|
|
258
262
|
}
|
|
259
263
|
catch (error) {
|
|
260
|
-
|
|
264
|
+
// Write directly to stderr to avoid routing through pino → OTel during an OTel failure
|
|
265
|
+
// (same pattern as setGlobalErrorHandler above).
|
|
266
|
+
process.stderr.write(`[OTel Log Flush Failed] ${JSON.stringify({
|
|
261
267
|
error: error instanceof Error ? error.message : String(error),
|
|
262
268
|
timeoutMs,
|
|
263
269
|
timestamp: new Date().toISOString(),
|
|
264
|
-
}));
|
|
270
|
+
})}\n`);
|
|
265
271
|
}
|
|
266
272
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import type { Logger } from 'pino';
|
|
3
|
+
/**
|
|
4
|
+
* Stores the current request-scoped child logger per async context.
|
|
5
|
+
* Used by `createContextAwareLogger` to make the `logger` export context-aware,
|
|
6
|
+
* and by `withLogContext` to set context for a block of code.
|
|
7
|
+
*/
|
|
8
|
+
export declare const loggerStorage: AsyncLocalStorage<Logger>;
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a base Pino logger in a Proxy that is aware of AsyncLocalStorage context.
|
|
11
|
+
* Called once at module init — the returned Proxy becomes the `logger` export.
|
|
12
|
+
*
|
|
13
|
+
* When code calls `logger.info(...)`, the Proxy intercepts the property access:
|
|
14
|
+
* 1. Checks `loggerStorage.getStore()` for a request-scoped child logger
|
|
15
|
+
* 2. If found, delegates to the child (which has user/org/request fields baked in)
|
|
16
|
+
* 3. If not found (e.g. during startup), falls back to the base logger
|
|
17
|
+
*
|
|
18
|
+
* This means all 39+ library files that `import { logger } from '@contrail/telemetry'`
|
|
19
|
+
* automatically get request-scoped context without any import changes.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // In telemetry/src/logger/index.ts (called once):
|
|
23
|
+
* export const logger = createContextAwareLogger(baseLogger);
|
|
24
|
+
*
|
|
25
|
+
* // In any library file:
|
|
26
|
+
* import { logger } from '@contrail/telemetry';
|
|
27
|
+
* logger.info('query executed'); // includes user.id, org.slug, etc. during a request
|
|
28
|
+
*
|
|
29
|
+
* ## How Proxy and Reflect work
|
|
30
|
+
*
|
|
31
|
+
* **Proxy** — wraps an object and intercepts operations (get, set, has, etc.).
|
|
32
|
+
* We only intercept `get` so that property reads (like `.info`, `.warn`, `.level`)
|
|
33
|
+
* are redirected to the context-appropriate logger.
|
|
34
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
|
35
|
+
*
|
|
36
|
+
* **Reflect.get(target, prop, receiver)** — performs the default property read.
|
|
37
|
+
* Using `Reflect.get` instead of `target[prop]` preserves correct `this` binding,
|
|
38
|
+
* which matters for Pino's internal method calls.
|
|
39
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
|
|
40
|
+
*/
|
|
41
|
+
export declare function createContextAwareLogger(baseLogger: Logger): Logger;
|
|
42
|
+
/**
|
|
43
|
+
* Runs `fn` with a child logger active in AsyncLocalStorage.
|
|
44
|
+
* Any code that calls `logger.info(...)` inside `fn` (including library code)
|
|
45
|
+
* will see the provided `fields` on its log output.
|
|
46
|
+
*
|
|
47
|
+
* Creates a child of the current context logger (or base logger if none),
|
|
48
|
+
* so nested calls stack correctly.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // In a NestJS interceptor:
|
|
52
|
+
* withLogContext({ user: { id: 'abc' }, url: { path: '/api/items' } }, () => {
|
|
53
|
+
* next.handle().subscribe(...);
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* // In a Lambda handler:
|
|
57
|
+
* withLogContext({ 'batch.id': batchId }, async () => {
|
|
58
|
+
* await processBatch(items);
|
|
59
|
+
* });
|
|
60
|
+
*/
|
|
61
|
+
export declare function withLogContext<T>(fields: Record<string, unknown>, fn: () => T): T;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loggerStorage = void 0;
|
|
4
|
+
exports.createContextAwareLogger = createContextAwareLogger;
|
|
5
|
+
exports.withLogContext = withLogContext;
|
|
6
|
+
const async_hooks_1 = require("async_hooks");
|
|
7
|
+
/**
|
|
8
|
+
* Stores the current request-scoped child logger per async context.
|
|
9
|
+
* Used by `createContextAwareLogger` to make the `logger` export context-aware,
|
|
10
|
+
* and by `withLogContext` to set context for a block of code.
|
|
11
|
+
*/
|
|
12
|
+
exports.loggerStorage = new async_hooks_1.AsyncLocalStorage();
|
|
13
|
+
let _baseLogger;
|
|
14
|
+
/**
|
|
15
|
+
* Wraps a base Pino logger in a Proxy that is aware of AsyncLocalStorage context.
|
|
16
|
+
* Called once at module init — the returned Proxy becomes the `logger` export.
|
|
17
|
+
*
|
|
18
|
+
* When code calls `logger.info(...)`, the Proxy intercepts the property access:
|
|
19
|
+
* 1. Checks `loggerStorage.getStore()` for a request-scoped child logger
|
|
20
|
+
* 2. If found, delegates to the child (which has user/org/request fields baked in)
|
|
21
|
+
* 3. If not found (e.g. during startup), falls back to the base logger
|
|
22
|
+
*
|
|
23
|
+
* This means all 39+ library files that `import { logger } from '@contrail/telemetry'`
|
|
24
|
+
* automatically get request-scoped context without any import changes.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // In telemetry/src/logger/index.ts (called once):
|
|
28
|
+
* export const logger = createContextAwareLogger(baseLogger);
|
|
29
|
+
*
|
|
30
|
+
* // In any library file:
|
|
31
|
+
* import { logger } from '@contrail/telemetry';
|
|
32
|
+
* logger.info('query executed'); // includes user.id, org.slug, etc. during a request
|
|
33
|
+
*
|
|
34
|
+
* ## How Proxy and Reflect work
|
|
35
|
+
*
|
|
36
|
+
* **Proxy** — wraps an object and intercepts operations (get, set, has, etc.).
|
|
37
|
+
* We only intercept `get` so that property reads (like `.info`, `.warn`, `.level`)
|
|
38
|
+
* are redirected to the context-appropriate logger.
|
|
39
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
|
40
|
+
*
|
|
41
|
+
* **Reflect.get(target, prop, receiver)** — performs the default property read.
|
|
42
|
+
* Using `Reflect.get` instead of `target[prop]` preserves correct `this` binding,
|
|
43
|
+
* which matters for Pino's internal method calls.
|
|
44
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
|
|
45
|
+
*/
|
|
46
|
+
function createContextAwareLogger(baseLogger) {
|
|
47
|
+
_baseLogger = baseLogger;
|
|
48
|
+
return new Proxy(baseLogger, {
|
|
49
|
+
get(target, prop, receiver) {
|
|
50
|
+
var _a;
|
|
51
|
+
const currentLogger = (_a = exports.loggerStorage.getStore()) !== null && _a !== void 0 ? _a : target;
|
|
52
|
+
return Reflect.get(currentLogger, prop, receiver);
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Runs `fn` with a child logger active in AsyncLocalStorage.
|
|
58
|
+
* Any code that calls `logger.info(...)` inside `fn` (including library code)
|
|
59
|
+
* will see the provided `fields` on its log output.
|
|
60
|
+
*
|
|
61
|
+
* Creates a child of the current context logger (or base logger if none),
|
|
62
|
+
* so nested calls stack correctly.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // In a NestJS interceptor:
|
|
66
|
+
* withLogContext({ user: { id: 'abc' }, url: { path: '/api/items' } }, () => {
|
|
67
|
+
* next.handle().subscribe(...);
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* // In a Lambda handler:
|
|
71
|
+
* withLogContext({ 'batch.id': batchId }, async () => {
|
|
72
|
+
* await processBatch(items);
|
|
73
|
+
* });
|
|
74
|
+
*/
|
|
75
|
+
function withLogContext(fields, fn) {
|
|
76
|
+
var _a;
|
|
77
|
+
const parent = (_a = exports.loggerStorage.getStore()) !== null && _a !== void 0 ? _a : _baseLogger;
|
|
78
|
+
const child = parent.child(fields);
|
|
79
|
+
return exports.loggerStorage.run(child, fn);
|
|
80
|
+
}
|