@contrail/telemetry 2.0.0 → 2.0.1-alpha-log-serialization-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.1] - 2026-04-14
11
+
12
+ ### Fixed
13
+
14
+ - **Error serialization** — `logger.error({ error }, 'msg')` and any other arbitrary key holding an `Error` object now serialize correctly (type, message, stack, enumerable properties) instead of logging `{}`. Handled via a `logMethod` hook that detects `Error` instances in the mergeObject and applies `pino.stdSerializers.err` before pino serializes. The `err` key (pino's built-in) and bare Error passed as the first argument were already working and continue to work unchanged.
15
+
10
16
  ## [2.0.0] - 2026-04-06
11
17
 
12
18
  ### Added
@@ -3,6 +3,7 @@ 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
5
  export { loggerStorage, withLogContext } from './log-context';
6
+ export declare const pinoErrorSerializationHooks: pino.LoggerOptions['hooks'];
6
7
  export declare const baseLogger: pino.Logger<never, boolean>;
7
8
  export declare const logger: pino.Logger;
8
9
  /**
@@ -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.pinoErrorSerializationHooks = 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"));
@@ -111,7 +111,7 @@ function createOtelStream() {
111
111
  var _a, _b;
112
112
  try {
113
113
  const logRecord = JSON.parse(chunk.toString());
114
- const { level, ...attributes } = logRecord;
114
+ const { level, [semantic_conventions_2.ATTR_LOG_MESSAGE]: message, ...attributes } = logRecord;
115
115
  const activeSpan = api_1.trace.getSpan(api_1.context.active());
116
116
  const spanContext = activeSpan === null || activeSpan === void 0 ? void 0 : activeSpan.spanContext();
117
117
  const spanAttributes = getSpanAttributes(activeSpan);
@@ -129,7 +129,7 @@ function createOtelStream() {
129
129
  otelLogger.emit({
130
130
  severityNumber: (_a = logger_config_1.PINO_LEVEL_TO_OTEL_SEVERITY[level]) !== null && _a !== void 0 ? _a : api_logs_1.SeverityNumber.INFO,
131
131
  severityText: (_b = logger_config_1.PINO_LEVEL_TO_NAME[level]) !== null && _b !== void 0 ? _b : 'INFO',
132
- body: attributes[semantic_conventions_2.ATTR_LOG_MESSAGE],
132
+ body: message,
133
133
  // Attribute precedence (last-spread-wins):
134
134
  // 1. safeAttributes — flattened user/log-context attributes (lowest priority)
135
135
  // 2. lambdaEnvAttributes — static Lambda env attributes (override user attrs)
@@ -222,11 +222,71 @@ function getLambdaRequestId() {
222
222
  }
223
223
  }
224
224
  // --- Base Pino Logger ---
225
+ // --- Error Serialization Hooks ---
226
+ /**
227
+ * Exported so tests can create a standalone pino instance with the same hooks.
228
+ *
229
+ * Fixes: Error objects passed under arbitrary keys (e.g. `{ error }`, `{ cause }`)
230
+ * serialize as `{}` because Error properties are non-enumerable.
231
+ *
232
+ * Pino already handles:
233
+ * - `{ err }` — via its default `serializers.err` (stdSerializers.err)
234
+ * - `logger.error(error, 'msg')` — pino wraps bare Error as `{ err: error }` internally
235
+ *
236
+ * This hook covers the remaining cases: any key whose value is an Error instance.
237
+ * It shallow-copies the mergeObject to avoid mutating the caller's object.
238
+ */
239
+ /**
240
+ * Walks a pino mergeObject and returns a new object with any Error-valued keys
241
+ * serialized via pino.stdSerializers.err. Returns null if no Errors are found
242
+ * (caller should use the original object unchanged).
243
+ *
244
+ * Skips the 'err' key — pino serializes it natively via its default serializers.err.
245
+ * Pre-serializing it here would cause double-serialization (plain object → type: "Object").
246
+ */
247
+ function serializeErrorsInMergeObject(mergeObject) {
248
+ let transformed = null;
249
+ for (const [key, value] of Object.entries(mergeObject)) {
250
+ if (!(value instanceof Error))
251
+ continue;
252
+ if (key === 'err')
253
+ continue;
254
+ if (!transformed)
255
+ transformed = { ...mergeObject };
256
+ transformed[key] = pino_1.default.stdSerializers.err(value);
257
+ }
258
+ return transformed;
259
+ }
260
+ exports.pinoErrorSerializationHooks = {
261
+ logMethod(inputArgs, method) {
262
+ if (inputArgs.length === 0)
263
+ return method.apply(this, inputArgs);
264
+ const [firstArg, ...rest] = inputArgs;
265
+ if (firstArg === null || firstArg === undefined) {
266
+ return method.apply(this, inputArgs);
267
+ }
268
+ if (firstArg instanceof Error) {
269
+ // Pino handles this internally: it wraps the bare Error as { err: error }
270
+ // before calling logMethod, so this branch is never actually reached.
271
+ // Guard is here for clarity and safety.
272
+ return method.apply(this, inputArgs);
273
+ }
274
+ if (typeof firstArg !== 'object') {
275
+ return method.apply(this, inputArgs);
276
+ }
277
+ const transformed = serializeErrorsInMergeObject(firstArg);
278
+ if (transformed) {
279
+ return method.apply(this, [transformed, ...rest]);
280
+ }
281
+ return method.apply(this, inputArgs);
282
+ },
283
+ };
225
284
  exports.baseLogger = (0, pino_1.default)({
226
285
  level: (_g = process.env.LOG_LEVEL) !== null && _g !== void 0 ? _g : 'debug',
227
286
  messageKey: semantic_conventions_2.ATTR_LOG_MESSAGE,
228
287
  timestamp: pino_1.default.stdTimeFunctions.isoTime,
229
288
  nestedKey: semantic_conventions_2.ATTR_LOG_PAYLOAD,
289
+ hooks: exports.pinoErrorSerializationHooks,
230
290
  base: {
231
291
  [semantic_conventions_1.ATTR_SERVICE_NAME]: serviceName,
232
292
  [semantic_conventions_4.ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: deploymentEnvironment,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/telemetry",
3
- "version": "2.0.0",
3
+ "version": "2.0.1-alpha-log-serialization-1",
4
4
  "description": "Telemetry and monitoring utilities for contrail services",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",