@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 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
@@ -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<never, boolean>;
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).
@@ -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
- exports.logger = exports.baseLogger;
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
- console.error('[OTel Log Flush Failed]', JSON.stringify({
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/telemetry",
3
- "version": "2.0.0-dev-telemetry-own-otel-1",
3
+ "version": "2.0.0",
4
4
  "description": "Telemetry and monitoring utilities for contrail services",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",