@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 +6 -0
- package/lib/logger/index.d.ts +1 -0
- package/lib/logger/index.js +63 -3
- package/package.json +1 -1
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
|
package/lib/logger/index.d.ts
CHANGED
|
@@ -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
|
/**
|
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.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:
|
|
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,
|