diff --git a/yarn-project/foundation/src/log/bigint-utils.ts b/yarn-project/foundation/src/log/bigint-utils.ts new file mode 100644 index 000000000000..6cc94101ac2f --- /dev/null +++ b/yarn-project/foundation/src/log/bigint-utils.ts @@ -0,0 +1,22 @@ +/** + * Converts bigint values to strings recursively in a log object to avoid serialization issues. + */ +export function convertBigintsToStrings(obj: unknown): unknown { + if (typeof obj === 'bigint') { + return String(obj); + } + + if (Array.isArray(obj)) { + return obj.map(item => convertBigintsToStrings(item)); + } + + if (obj !== null && typeof obj === 'object') { + const result: Record = {}; + for (const key in obj) { + result[key] = convertBigintsToStrings((obj as Record)[key]); + } + return result; + } + + return obj; +} diff --git a/yarn-project/foundation/src/log/gcloud-logger-config.ts b/yarn-project/foundation/src/log/gcloud-logger-config.ts index db3b331141df..2e036212af71 100644 --- a/yarn-project/foundation/src/log/gcloud-logger-config.ts +++ b/yarn-project/foundation/src/log/gcloud-logger-config.ts @@ -1,5 +1,7 @@ import type { pino } from 'pino'; +import { convertBigintsToStrings } from './bigint-utils.js'; + /* eslint-disable camelcase */ const GOOGLE_CLOUD_TRACE_ID = 'logging.googleapis.com/trace'; @@ -15,6 +17,9 @@ export const GoogleCloudLoggerConfig = { messageKey: 'message', formatters: { log(object: Record): Record { + // Convert bigints to strings recursively to avoid serialization issues + object = convertBigintsToStrings(object) as Record; + // Add trace context attributes following Cloud Logging structured log format described // in https://cloud.google.com/logging/docs/structured-logging#special-payload-fields const { trace_id, span_id, trace_flags, ...rest } = object; diff --git a/yarn-project/foundation/src/log/pino-logger.test.ts b/yarn-project/foundation/src/log/pino-logger.test.ts index 1efdcf038f18..9881535d4f58 100644 --- a/yarn-project/foundation/src/log/pino-logger.test.ts +++ b/yarn-project/foundation/src/log/pino-logger.test.ts @@ -188,6 +188,91 @@ describe('pino-logger', () => { }); }); + it('converts bigints to strings recursively ', () => { + const testLogger = createLogger('bigint-test'); + capturingStream.clear(); + + testLogger.info('comprehensive bigint conversion', { + // Top-level bigints + amount: 123456789012345678901234n, + slot: 42n, + // Nested objects + nested: { + value: 999999999999999999n, + deepNested: { + id: 12345678901234567890n, + }, + }, + // Arrays with bigints + array: [1n, 2n, 3n], + mixedArray: [{ id: 999n }, { id: 888n }], + // Mixed types + numberValue: 123, + stringValue: 'test', + boolValue: true, + nullValue: null, + }); + + const entries = capturingStream.getJsonLines(); + expect(entries).toHaveLength(1); + expect(entries[0]).toMatchObject({ + module: 'bigint-test', + msg: 'comprehensive bigint conversion', + // All bigints converted to strings + amount: '123456789012345678901234', + slot: '42', + nested: { + value: '999999999999999999', + deepNested: { + id: '12345678901234567890', + }, + }, + array: ['1', '2', '3'], + mixedArray: [{ id: '999' }, { id: '888' }], + // Other types preserved + numberValue: 123, + stringValue: 'test', + boolValue: true, + nullValue: null, + }); + }); + + it('does not mutate the original log data object', () => { + const testLogger = createLogger('mutation-test'); + capturingStream.clear(); + + const originalData = { + amount: 123456789012345678901234n, + nested: { + value: 999n, + }, + array: [1n, 2n, 3n], + }; + + // Keep references to verify mutation + const originalAmount = originalData.amount; + const originalNestedValue = originalData.nested.value; + const originalArrayItem = originalData.array[0]; + + testLogger.info('mutation test', originalData); + + // Verify the original object was NOT mutated + expect(originalData.amount).toBe(originalAmount); + expect(typeof originalData.amount).toBe('bigint'); + expect(originalData.nested.value).toBe(originalNestedValue); + expect(typeof originalData.nested.value).toBe('bigint'); + expect(originalData.array[0]).toBe(originalArrayItem); + expect(typeof originalData.array[0]).toBe('bigint'); + + // But the logged version should have strings + const entries = capturingStream.getJsonLines(); + expect(entries[0]).toMatchObject({ + amount: '123456789012345678901234', + nested: { value: '999' }, + array: ['1', '2', '3'], + }); + }); + it('returns bindings via getBindings', () => { const testLogger = createLogger('bindings-test', { actor: 'main', instanceId: 'id-123' }); const bindings = testLogger.getBindings(); diff --git a/yarn-project/foundation/src/log/pino-logger.ts b/yarn-project/foundation/src/log/pino-logger.ts index 2a41d44bf117..2395cc908ec0 100644 --- a/yarn-project/foundation/src/log/pino-logger.ts +++ b/yarn-project/foundation/src/log/pino-logger.ts @@ -7,6 +7,7 @@ import { inspect } from 'util'; import { compactArray } from '../collection/array.js'; import type { EnvVar } from '../config/index.js'; import { parseBooleanEnv } from '../config/parse-env.js'; +import { convertBigintsToStrings } from './bigint-utils.js'; import { GoogleCloudLoggerConfig } from './gcloud-logger-config.js'; import { getLogLevelFromFilters, parseEnv } from './log-filters.js'; import type { LogLevel } from './log-levels.js'; @@ -165,6 +166,9 @@ const pinoOpts: pino.LoggerOptions = { ...redactedPaths.map(p => `opts.${p}`), ], }, + formatters: { + log: obj => convertBigintsToStrings(obj) as Record, + }, ...(useGcloudLogging ? GoogleCloudLoggerConfig : {}), }; diff --git a/yarn-project/slasher/src/slash_offenses_collector.ts b/yarn-project/slasher/src/slash_offenses_collector.ts index 274357a97c6a..551f868ccec3 100644 --- a/yarn-project/slasher/src/slash_offenses_collector.ts +++ b/yarn-project/slasher/src/slash_offenses_collector.ts @@ -85,11 +85,7 @@ export class SlashOffensesCollector { } } - this.log.info(`Adding pending offense for validator ${arg.validator}`, { - ...pendingOffense, - epochOrSlot: pendingOffense.epochOrSlot.toString(), - amount: pendingOffense.amount.toString(), - }); + this.log.info(`Adding pending offense for validator ${arg.validator}`, pendingOffense); await this.offensesStore.addPendingOffense(pendingOffense); } }