Skip to content

Commit 6e33cb9

Browse files
authored
chore: Sync logging with jest (#10459)
Jest would overwrite pino logs when logging via a worker thread. This changes logging so that we only use pino-pretty if running in jest, and we only use it as a stream in the main loop. See https://github.com/pinojs/pino-pretty?tab=readme-ov-file#usage-with-jest for more info. This PR also restores OTLP exports via `getEndToEndTestTelemetryClient`, which had been broken in a previous PR. Export now works by using a multistream destination (again, main loop) to send logs to OTEL. Note that there was an error in how we created the telemetry clients: since starting a new OTEL telemetry client involved registering metrics/traces/logs in the global OTEL variables, there were multiple errors in the lines of `Attempted duplicate registration of API: trace`. This is now fixed by having a single test telemetry client instance. This requires _not_ setting the service name, which is unfortunate. ![image](https://github.com/user-attachments/assets/5208178c-7765-444f-b0f9-e3f4b7532e43) As a last change here, the jest reporter for end-to-end tests is now set to `summary`. This means that jest will no longer write to the console `RUNS my-test-suite` every second or so, overwriting the actual logs we are trying to read.
1 parent 92eb377 commit 6e33cb9

File tree

8 files changed

+99
-48
lines changed

8 files changed

+99
-48
lines changed

yarn-project/end-to-end/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,9 @@
145145
},
146146
"reporters": [
147147
[
148-
"default",
148+
"summary",
149149
{
150-
"summaryThreshold": 9999
150+
"summaryThreshold": 0
151151
}
152152
]
153153
],

yarn-project/end-to-end/package.local.json

+10
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,15 @@
44
"formatting": "run -T prettier --check ./src \"!src/web/main.js\" && run -T eslint ./src",
55
"test": "LOG_LEVEL=${LOG_LEVEL:-verbose} NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --testTimeout=300000 --forceExit",
66
"test:unit": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest src/fixtures"
7+
},
8+
"jest": {
9+
"reporters": [
10+
[
11+
"summary",
12+
{
13+
"summaryThreshold": 0
14+
}
15+
]
16+
]
717
}
818
}

yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export class P2PNetworkTest {
121121
}) {
122122
const port = basePort || (await getPort());
123123

124-
const telemetry = await getEndToEndTestTelemetryClient(metricsPort, /*service name*/ `bootstrapnode`);
124+
const telemetry = await getEndToEndTestTelemetryClient(metricsPort);
125125
const bootstrapNode = await createBootstrapNodeFromPrivateKey(BOOTSTRAP_NODE_PRIVATE_KEY, port, telemetry);
126126
const bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt();
127127

yarn-project/end-to-end/src/fixtures/setup_p2p_test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function createNode(
6363
) {
6464
const validatorConfig = await createValidatorConfig(config, bootstrapNode, tcpPort, accountIndex, dataDirectory);
6565

66-
const telemetryClient = await getEndToEndTestTelemetryClient(metricsPort, /*serviceName*/ `node:${tcpPort}`);
66+
const telemetryClient = await getEndToEndTestTelemetryClient(metricsPort);
6767

6868
return await AztecNodeService.createAndSync(validatorConfig, {
6969
telemetry: telemetryClient,

yarn-project/end-to-end/src/fixtures/snapshot_manager.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ async function setupFromFresh(
351351
aztecNodeConfig.bbWorkingDirectory = bbConfig.bbWorkingDirectory;
352352
}
353353

354-
const telemetry = await getEndToEndTestTelemetryClient(opts.metricsPort, /*serviceName*/ statePath);
354+
const telemetry = await getEndToEndTestTelemetryClient(opts.metricsPort);
355355

356356
logger.verbose('Creating and synching an aztec node...');
357357
const aztecNode = await AztecNodeService.createAndSync(aztecNodeConfig, { telemetry });
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,36 @@
1+
import { levels, registerLoggingStream } from '@aztec/foundation/log';
12
import { type TelemetryClient } from '@aztec/telemetry-client';
23
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
4+
import { OTelPinoStream } from '@aztec/telemetry-client/otel-pino-stream';
35
import {
46
type TelemetryClientConfig,
57
createAndStartTelemetryClient,
68
getConfigEnvVars as getTelemetryConfig,
79
} from '@aztec/telemetry-client/start';
810

9-
export function getEndToEndTestTelemetryClient(metricsPort?: number, serviceName?: string): Promise<TelemetryClient> {
10-
return !metricsPort
11-
? Promise.resolve(new NoopTelemetryClient())
12-
: createAndStartTelemetryClient(getEndToEndTestTelemetryConfig(metricsPort, serviceName));
11+
let telemetryClient: Promise<TelemetryClient> | undefined;
12+
export function getEndToEndTestTelemetryClient(metricsPort?: number): Promise<TelemetryClient> {
13+
if (!metricsPort) {
14+
return Promise.resolve(new NoopTelemetryClient());
15+
}
16+
if (!telemetryClient) {
17+
telemetryClient = createEndToEndTestOtelClient(metricsPort);
18+
}
19+
return telemetryClient;
20+
}
21+
22+
function createEndToEndTestOtelClient(metricsPort: number): Promise<TelemetryClient> {
23+
const otelStream = new OTelPinoStream({ levels });
24+
registerLoggingStream(otelStream);
25+
return createAndStartTelemetryClient(getEndToEndTestTelemetryConfig(metricsPort));
1326
}
1427

1528
/**
1629
* Utility functions for setting up end-to-end tests with telemetry.
1730
*
1831
* Read from env vars, override if metricsPort is set
1932
*/
20-
export function getEndToEndTestTelemetryConfig(metricsPort?: number, serviceName?: string) {
33+
function getEndToEndTestTelemetryConfig(metricsPort?: number) {
2134
const telemetryConfig: TelemetryClientConfig = getTelemetryConfig();
2235
if (metricsPort) {
2336
telemetryConfig.metricsCollectorUrl = new URL(`http://127.0.0.1:${metricsPort}/v1/metrics`);
@@ -27,8 +40,5 @@ export function getEndToEndTestTelemetryConfig(metricsPort?: number, serviceName
2740
telemetryConfig.otelCollectIntervalMs = 5000;
2841
telemetryConfig.otelExportTimeoutMs = 2500;
2942
}
30-
if (serviceName) {
31-
telemetryConfig.serviceName = serviceName;
32-
}
3343
return telemetryConfig;
3444
}

yarn-project/foundation/src/log/pino-logger.ts

+63-32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { createColors } from 'colorette';
22
import isNode from 'detect-node';
3-
import { type LoggerOptions, pino } from 'pino';
3+
import { pino, symbols } from 'pino';
4+
import pretty from 'pino-pretty';
5+
import { type Writable } from 'stream';
46
import { inspect } from 'util';
57

68
import { compactArray } from '../collection/array.js';
@@ -67,32 +69,35 @@ function isLevelEnabled(logger: pino.Logger<'verbose', boolean>, level: LogLevel
6769
const defaultLogLevel = process.env.NODE_ENV === 'test' ? 'silent' : 'info';
6870
const [logLevel, logFilters] = parseEnv(process.env.LOG_LEVEL, defaultLogLevel);
6971

70-
// Transport options for pretty logging to stdout via pino-pretty.
72+
// Transport options for pretty logging to stderr via pino-pretty.
7173
const useColor = true;
7274
const { bold, reset } = createColors({ useColor });
73-
const prettyTransport: LoggerOptions['transport'] = {
75+
const pinoPrettyOpts = {
76+
destination: 2,
77+
sync: true,
78+
colorize: useColor,
79+
ignore: 'module,pid,hostname,trace_id,span_id,trace_flags',
80+
messageFormat: `${bold('{module}')} ${reset('{msg}')}`,
81+
customLevels: 'fatal:60,error:50,warn:40,info:30,verbose:25,debug:20,trace:10',
82+
customColors: 'fatal:bgRed,error:red,warn:yellow,info:green,verbose:magenta,debug:blue,trace:gray',
83+
minimumLevel: 'trace' as const,
84+
};
85+
const prettyTransport: pino.TransportSingleOptions = {
7486
target: 'pino-pretty',
75-
options: {
76-
destination: 2,
77-
sync: true,
78-
colorize: useColor,
79-
ignore: 'module,pid,hostname,trace_id,span_id,trace_flags',
80-
messageFormat: `${bold('{module}')} ${reset('{msg}')}`,
81-
customLevels: 'fatal:60,error:50,warn:40,info:30,verbose:25,debug:20,trace:10',
82-
customColors: 'fatal:bgRed,error:red,warn:yellow,info:green,verbose:magenta,debug:blue,trace:gray',
83-
},
87+
options: pinoPrettyOpts,
8488
};
8589

8690
// Transport for vanilla stdio logging as JSON.
87-
const stdioTransport: LoggerOptions['transport'] = {
91+
const stdioTransport: pino.TransportSingleOptions = {
8892
target: 'pino/file',
8993
options: { destination: 2 },
9094
};
9195

9296
// Define custom logging levels for pino.
9397
const customLevels = { verbose: 25 };
9498
const pinoOpts = { customLevels, useOnlyCustomLevels: false, level: logLevel };
95-
const levels = {
99+
100+
export const levels = {
96101
labels: { ...pino.levels.labels, ...Object.fromEntries(Object.entries(customLevels).map(e => e.reverse())) },
97102
values: { ...pino.levels.values, ...customLevels },
98103
};
@@ -103,27 +108,32 @@ const levels = {
103108
// would mean that all child loggers created before the telemetry-client is initialized would not have
104109
// this transport configured. Note that the target is defined as the export in the telemetry-client,
105110
// since pino will load this transport separately on a worker thread, to minimize disruption to the main loop.
106-
107-
const otelTransport: LoggerOptions['transport'] = {
111+
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
112+
const otelOpts = { levels };
113+
const otelTransport: pino.TransportSingleOptions = {
108114
target: '@aztec/telemetry-client/otel-pino-stream',
109-
options: { levels, messageKey: 'msg' },
115+
options: otelOpts,
110116
};
111117

112-
// In nodejs, create a new pino instance with an stdout transport (either vanilla or json), and optionally
113-
// an OTLP transport if the OTLP endpoint is provided. Note that transports are initialized in a worker thread.
114-
// On the browser, we just log to the console.
115-
const otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
116-
const logger = isNode
117-
? pino(
118-
pinoOpts,
119-
pino.transport({
120-
targets: compactArray([
121-
['1', 'true', 'TRUE'].includes(process.env.LOG_JSON ?? '') ? stdioTransport : prettyTransport,
122-
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ? otelTransport : undefined,
123-
]),
124-
}),
125-
)
126-
: pino({ ...pinoOpts, browser: { asObject: false } });
118+
function makeLogger() {
119+
if (!isNode) {
120+
// We are on the browser
121+
return pino({ ...pinoOpts, browser: { asObject: false } });
122+
} else if (process.env.JEST_WORKER_ID) {
123+
// We are on jest, so we need sync logging. We stream to stderr with pretty.
124+
return pino(pinoOpts, pretty(pinoPrettyOpts));
125+
} else {
126+
// Regular nodejs with transports on worker thread, using pino-pretty for console logging if LOG_JSON
127+
// is not set, and an optional OTLP transport if the OTLP endpoint is provided.
128+
const targets: pino.TransportSingleOptions[] = compactArray([
129+
['1', 'true', 'TRUE'].includes(process.env.LOG_JSON ?? '') ? stdioTransport : prettyTransport,
130+
otlpEndpoint ? otelTransport : undefined,
131+
]);
132+
return pino(pinoOpts, pino.transport({ targets }));
133+
}
134+
}
135+
136+
const logger = makeLogger();
127137

128138
// Log the logger configuration.
129139
logger.verbose(
@@ -136,6 +146,27 @@ logger.verbose(
136146
: `Browser console logger initialized with level ${logLevel}`,
137147
);
138148

149+
/**
150+
* Registers an additional destination to the pino logger.
151+
* Use only when working with destinations, not worker transports.
152+
*/
153+
export function registerLoggingStream(stream: Writable): void {
154+
logger.verbose({ module: 'logger' }, `Registering additional logging stream`);
155+
const original = (logger as any)[symbols.streamSym];
156+
const destination = original
157+
? pino.multistream(
158+
[
159+
// Set streams to lowest logging level, and control actual logging from the parent logger
160+
// otherwise streams default to info and refuse to log anything below that.
161+
{ level: 'trace', stream: original },
162+
{ level: 'trace', stream },
163+
],
164+
{ levels: levels.values },
165+
)
166+
: stream;
167+
(logger as any)[symbols.streamSym] = destination;
168+
}
169+
139170
/** Log function that accepts an exception object */
140171
type ErrorLogFn = (msg: string, err?: Error | unknown, data?: LogData) => void;
141172

yarn-project/telemetry-client/src/vendor/otel-pino-stream.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ export function getTimeConverter(pinoLogger: any, pinoMod: any) {
129129
}
130130

131131
interface OTelPinoStreamOptions {
132-
messageKey: string;
132+
messageKey?: string;
133133
levels: any; // Pino.LevelMapping
134-
otelTimestampFromTime: (time: any) => number;
134+
otelTimestampFromTime?: (time: any) => number;
135135
}
136136

137137
/**
@@ -153,7 +153,7 @@ export class OTelPinoStream extends Writable {
153153
// to transports. Eventually OTelPinoStream might be able to use this
154154
// for auto-configuration in newer pino versions. The event currently does
155155
// not include the `timeSym` value that is needed here, however.
156-
this._messageKey = options.messageKey;
156+
this._messageKey = options.messageKey ?? 'msg';
157157
this._levels = options.levels;
158158

159159
// [aztec] The following will break if we set up a custom time function in our logger

0 commit comments

Comments
 (0)