@contrail/telemetry 2.0.3 → 2.0.4-alpha-formatted-logs.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,6 +7,12 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.4] - 2026-04-16
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
+
10
16
  ## [2.0.3] - 2026-04-15
11
17
 
12
18
  ### Changed
@@ -32,6 +32,31 @@ Object.defineProperty(exports, "ATTR_LOG_PAYLOAD", { enumerable: true, get: func
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
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
+ const globalRecord = globalThis;
51
+ if (globalRecord[TELEMETRY_SINGLETON_KEY]) {
52
+ process.stderr.write('[WARN] @contrail/telemetry loaded twice — duplicate module instance detected. ' +
53
+ 'flushLogs() will only flush one provider and log context may not propagate correctly. ' +
54
+ 'Ensure @contrail/telemetry is listed as a peerDependency in any library that re-exports ' +
55
+ 'it, and that the host service lists it in its own package.json.\n');
56
+ }
57
+ else {
58
+ globalRecord[TELEMETRY_SINGLETON_KEY] = true;
59
+ }
35
60
  // --- Environment & Resource Setup ---
36
61
  const semantic_conventions_4 = require("../semantic-conventions");
37
62
  const LAMBDA_ENV_OTEL_ATTRIBUTES = getLambdaEnvAttributes();
@@ -57,6 +82,18 @@ const otelLoggerProvider = new sdk_logs_1.LoggerProvider({
57
82
  });
58
83
  const otelLogger = otelLoggerProvider.getLogger(serviceName);
59
84
  // --- Pino Stream Destinations ---
85
+ // Capture the original console.log before app-framework's hijackConsole() replaces it.
86
+ // Lambda treats each console.log() call as a single CloudWatch log event (even with
87
+ // embedded newlines), unlike process.stdout.write() which splits on \n. Using the
88
+ // original lets us pretty-print objects without them scattering across log events.
89
+ //
90
+ // Guard: if telemetry is imported after hijackConsole(), the captured function would be
91
+ // the hijacked one (named "overridenLog"), creating an infinite loop. Fall back to
92
+ // process.stdout.write in that case — loses single-event grouping but stays safe.
93
+ const _capturedConsoleLog = console.log;
94
+ const originalConsoleLog = _capturedConsoleLog.name === 'overridenLog'
95
+ ? (...args) => process.stdout.write(args.map(String).join(' ') + '\n')
96
+ : _capturedConsoleLog;
60
97
  function isPinoPrettyInstalled() {
61
98
  try {
62
99
  require.resolve('pino-pretty');
@@ -92,14 +129,14 @@ function createStdoutStream() {
92
129
  const requestId = (_d = getLambdaRequestId()) !== null && _d !== void 0 ? _d : '-';
93
130
  const payload = logRecord[semantic_conventions_2.ATTR_LOG_PAYLOAD];
94
131
  const payloadLine = payload && typeof payload === 'object' && Object.keys(payload).length > 0
95
- ? `${JSON.stringify(payload, null, 2)}\n`
132
+ ? `\n${JSON.stringify(payload, null, 2)}`
96
133
  : '';
97
- line = `${time}\t${requestId}\t${level}\t${message}\n${payloadLine}`;
134
+ line = `${time}\t${requestId}\t${level}\t${message}${payloadLine}`;
98
135
  }
99
136
  catch {
100
137
  line = chunk.toString();
101
138
  }
102
- process.stdout.write(line);
139
+ originalConsoleLog(line);
103
140
  callback();
104
141
  },
105
142
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/telemetry",
3
- "version": "2.0.3",
3
+ "version": "2.0.4-alpha-formatted-logs.1",
4
4
  "description": "Telemetry and monitoring utilities for contrail services",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",