@contrail/telemetry 2.0.2 → 2.0.4-alpha.1

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,7 +7,20 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [2.0.1] - 2026-04-15
10
+ ## [2.0.4] - 2026-04-15
11
+
12
+ ### Added
13
+
14
+ - **Duplicate module detection** — logs a `stderr` warning at cold start if two copies of `@contrail/telemetry` are loaded, so silent log loss and broken context propagation are immediately visible.
15
+
16
+ ## [2.0.3] - 2026-04-15
17
+
18
+ ### Changed
19
+
20
+ - **`withLogContext` renamed to `withLogAttributes`** — Aligns the export name with the API documented in 2.0.0. Any code importing `withLogContext` directly must update to `withLogAttributes`.
21
+ - **`addStream` fixed** — Uses `pino.symbols.streamSym` to access the underlying multistream, replacing an internal `(logger as any).stream` access that broke across pino versions.
22
+
23
+ ## [2.0.2] - 2026-04-15
11
24
 
12
25
  ### Fixed
13
26
 
@@ -18,7 +31,7 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
18
31
  ### Added
19
32
 
20
33
  - **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.
21
- - **`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.
34
+ - **`withLogAttributes(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.
22
35
  - **`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).
23
36
  - **`flushLogs()`** — Flush pending logs to the OTLP collector before a Lambda execution environment freezes.
24
37
  - **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.
@@ -2,7 +2,7 @@ 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
+ export { loggerStorage, withLogAttributes } from './log-context';
6
6
  export declare const baseLogger: pino.Logger<never, boolean>;
7
7
  export declare const logger: pino.Logger;
8
8
  /**
@@ -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.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;
7
+ exports.logger = exports.baseLogger = exports.withLogAttributes = 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"));
@@ -31,7 +31,31 @@ Object.defineProperty(exports, "ATTR_LOG_MESSAGE", { enumerable: true, get: func
31
31
  Object.defineProperty(exports, "ATTR_LOG_PAYLOAD", { enumerable: true, get: function () { return semantic_conventions_3.ATTR_LOG_PAYLOAD; } });
32
32
  var log_context_1 = require("./log-context");
33
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; } });
34
+ Object.defineProperty(exports, "withLogAttributes", { enumerable: true, get: function () { return log_context_1.withLogAttributes; } });
35
+ // --- Singleton Guard ---
36
+ // The logger uses AsyncLocalStorage for request context propagation. If two copies
37
+ // of this module are loaded (e.g. due to a version mismatch causing npm to install
38
+ // a nested duplicate), each copy has its own AsyncLocalStorage instance and its own
39
+ // OTel LoggerProvider. Context set in one copy won't be visible in the other, and
40
+ // flushLogs() will only flush the provider it closes over — silently dropping logs.
41
+ //
42
+ // Symbol.for uses the global symbol registry, which is shared across all module
43
+ // instances, so this check fires correctly even when two copies are loaded.
44
+ //
45
+ // Local dev note: running `npm i` inside a lib package directory causes npm v7+ to
46
+ // auto-install peer deps locally. If that package is then symlinked into a service,
47
+ // the nested node_modules travels with it and re-creates the duplicate. Published
48
+ // tarballs don't ship node_modules, so this is local-only.
49
+ const TELEMETRY_SINGLETON_KEY = Symbol.for('@contrail/telemetry/logger-initialized');
50
+ if (global[TELEMETRY_SINGLETON_KEY]) {
51
+ process.stderr.write('[WARN] @contrail/telemetry loaded twice — duplicate module instance detected. ' +
52
+ 'flushLogs() will only flush one provider and log context may not propagate correctly. ' +
53
+ 'Ensure @contrail/telemetry is listed as a peerDependency in any library that re-exports ' +
54
+ 'it, and that the host service lists it in its own package.json.\n');
55
+ }
56
+ else {
57
+ global[TELEMETRY_SINGLETON_KEY] = true;
58
+ }
35
59
  // --- Environment & Resource Setup ---
36
60
  const semantic_conventions_4 = require("../semantic-conventions");
37
61
  const LAMBDA_ENV_OTEL_ATTRIBUTES = getLambdaEnvAttributes();
@@ -243,7 +267,8 @@ exports.logger = (0, log_context_2.createContextAwareLogger)(exports.baseLogger)
243
267
  * Use this to wire additional log sinks (e.g. InMemoryLogStream in app-framework).
244
268
  */
245
269
  function addStream(streamEntry) {
246
- const ms = exports.baseLogger.stream;
270
+ const streamSym = pino_1.default.symbols.streamSym;
271
+ const ms = exports.baseLogger[streamSym];
247
272
  if (ms && typeof ms.add === 'function') {
248
273
  ms.add(streamEntry);
249
274
  }
@@ -3,7 +3,7 @@ import type { Logger } from 'pino';
3
3
  /**
4
4
  * Stores the current request-scoped child logger per async context.
5
5
  * Used by `createContextAwareLogger` to make the `logger` export context-aware,
6
- * and by `withLogContext` to set context for a block of code.
6
+ * and by `withLogAttributes` to set context for a block of code.
7
7
  */
8
8
  export declare const loggerStorage: AsyncLocalStorage<Logger>;
9
9
  /**
@@ -49,13 +49,13 @@ export declare function createContextAwareLogger(baseLogger: Logger): Logger;
49
49
  *
50
50
  * @example
51
51
  * // In a NestJS interceptor:
52
- * withLogContext({ user: { id: 'abc' }, url: { path: '/api/items' } }, () => {
52
+ * withLogAttributes({ user: { id: 'abc' }, url: { path: '/api/items' } }, () => {
53
53
  * next.handle().subscribe(...);
54
54
  * });
55
55
  *
56
56
  * // In a Lambda handler:
57
- * withLogContext({ 'batch.id': batchId }, async () => {
57
+ * withLogAttributes({ 'batch.id': batchId }, async () => {
58
58
  * await processBatch(items);
59
59
  * });
60
60
  */
61
- export declare function withLogContext<T>(fields: Record<string, unknown>, fn: () => T): T;
61
+ export declare function withLogAttributes<T>(fields: Record<string, unknown>, fn: () => T): T;
@@ -2,12 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loggerStorage = void 0;
4
4
  exports.createContextAwareLogger = createContextAwareLogger;
5
- exports.withLogContext = withLogContext;
5
+ exports.withLogAttributes = withLogAttributes;
6
6
  const async_hooks_1 = require("async_hooks");
7
7
  /**
8
8
  * Stores the current request-scoped child logger per async context.
9
9
  * Used by `createContextAwareLogger` to make the `logger` export context-aware,
10
- * and by `withLogContext` to set context for a block of code.
10
+ * and by `withLogAttributes` to set context for a block of code.
11
11
  */
12
12
  exports.loggerStorage = new async_hooks_1.AsyncLocalStorage();
13
13
  let _baseLogger;
@@ -63,16 +63,16 @@ function createContextAwareLogger(baseLogger) {
63
63
  *
64
64
  * @example
65
65
  * // In a NestJS interceptor:
66
- * withLogContext({ user: { id: 'abc' }, url: { path: '/api/items' } }, () => {
66
+ * withLogAttributes({ user: { id: 'abc' }, url: { path: '/api/items' } }, () => {
67
67
  * next.handle().subscribe(...);
68
68
  * });
69
69
  *
70
70
  * // In a Lambda handler:
71
- * withLogContext({ 'batch.id': batchId }, async () => {
71
+ * withLogAttributes({ 'batch.id': batchId }, async () => {
72
72
  * await processBatch(items);
73
73
  * });
74
74
  */
75
- function withLogContext(fields, fn) {
75
+ function withLogAttributes(fields, fn) {
76
76
  var _a;
77
77
  const parent = (_a = exports.loggerStorage.getStore()) !== null && _a !== void 0 ? _a : _baseLogger;
78
78
  const child = parent.child(fields);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/telemetry",
3
- "version": "2.0.2",
3
+ "version": "2.0.4-alpha.1",
4
4
  "description": "Telemetry and monitoring utilities for contrail services",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",