diff --git a/packages/js-logger/README.md b/packages/js-logger/README.md index 7e96e251..03ba91de 100644 --- a/packages/js-logger/README.md +++ b/packages/js-logger/README.md @@ -51,12 +51,30 @@ For more detailed usage check the [pino documentation](https://github.com/pinojs | opentelemetryOptions.url | string | `http://localhost:4317` | OpenTelemetry gRPC collector URL, for example an Alloy OTLP endpoint. | | opentelemetryOptions.resourceAttributes | object | undefined | Extra resource attributes merged into the emitted OpenTelemetry resource. | -The second argument controls the output destination and defaults to standard output (`1`). +The second argument controls the output destination - either a file path or a file descriptor number. It defaults to standard output (`1`). + +Once created, the logger emits a `logger initialized` message at `debug` level summarizing the settings it started with: level, `prettyPrint`, `pinoCaller`, and whether OpenTelemetry is enabled along with its URL. ## OpenTelemetry log support -When `opentelemetryOptions.enabled` is `true`, `js-logger` switches to `pino-opentelemetry-transport` and populates resource attributes from: +When `opentelemetryOptions.enabled` is `true`, `js-logger` adds `pino-opentelemetry-transport` alongside the regular destination, so logs are still written locally as well as exported. Resource attributes are populated from: - the current package name and version - detected container metadata - `process.env.K8S_POD_UID` +- any `resourceAttributes` you pass, which override the detected values + +```typescript +import { jsLogger } from '@map-colonies/js-logger'; + +const logger = await jsLogger({ + level: 'info', + opentelemetryOptions: { + enabled: true, + url: 'http://otel-collector:4317', + resourceAttributes: { 'deployment.environment': 'production' }, + }, +}); +``` + +Note that with OpenTelemetry enabled the `level` field is emitted as its numeric pino value (`30`) rather than the label (`"info"`), since both the OpenTelemetry transport and the routing between the two targets rely on numeric levels. diff --git a/packages/js-logger/src/index.ts b/packages/js-logger/src/index.ts index 771cc200..5d7d4f4f 100644 --- a/packages/js-logger/src/index.ts +++ b/packages/js-logger/src/index.ts @@ -3,6 +3,7 @@ import { type LoggerOptions as PinoOptions, type Logger, type TransportSingleOptions, + type TransportMultiOptions, transport as pinoTransport, type DestinationStream, } from 'pino'; @@ -72,6 +73,8 @@ interface LoggerOptions { }; } +const DEFAULT_OTEL_URL = 'http://localhost:4317'; + const baseOptions: PinoOptions = { formatters: { level(label): Record { @@ -89,16 +92,22 @@ const baseOptions: PinoOptions = { * @public */ export async function jsLogger(options?: LoggerOptions, destination: string | number = 1): Promise { - let transport: TransportSingleOptions = { target: 'pino/file', options: { destination } }; + // captured before the deletes below remove them from options + const prettyPrintEnabled = options?.prettyPrint === true; + const otelEnabled = options?.opentelemetryOptions?.enabled === true; + const otelUrl = options?.opentelemetryOptions?.url ?? DEFAULT_OTEL_URL; /* istanbul ignore next */ - if (options?.prettyPrint === true) { - transport = { target: 'pino-pretty' }; - + if (prettyPrintEnabled) { delete options.prettyPrint; } - if (options?.opentelemetryOptions?.enabled === true) { + // where logs are written locally. when otel is enabled this becomes the second target alongside it. + const localTransport: TransportSingleOptions = prettyPrintEnabled ? { target: 'pino-pretty' } : { target: 'pino/file', options: { destination } }; + + let transportOptions: TransportSingleOptions | TransportMultiOptions = localTransport; + + if (otelEnabled) { const pkg = readPackageJsonSync(); const detectedResources = detectResources({ detectors: [containerDetector] }); @@ -112,33 +121,38 @@ export async function jsLogger(options?: LoggerOptions, destination: string | nu [ATTR_SERVICE_NAME]: pkg.name, [ATTR_SERVICE_VERSION]: pkg.version, [ATTR_K8S_POD_UID]: process.env.K8S_POD_UID, - ...options.opentelemetryOptions.resourceAttributes, + ...options.opentelemetryOptions?.resourceAttributes, }, logRecordProcessorOptions: [ - { - recordProcessorType: 'simple', - exporterOptions: { - protocol: 'console', - }, - }, { recordProcessorType: 'batch', - exporterOptions: { protocol: 'grpc', grpcExporterOptions: { url: options.opentelemetryOptions.url ?? 'http://localhost:4317' } }, + exporterOptions: { protocol: 'grpc', grpcExporterOptions: { url: otelUrl } }, }, ], }; - transport = { target: 'pino-opentelemetry-transport', options: otelOptions }; + // routing between multiple targets happens by level in a worker, which requires the numeric levels left in place by + // dropping the formatter - so this must stay limited to the otel path. delete baseOptions.formatters; + + transportOptions = { targets: [{ target: 'pino-opentelemetry-transport', options: otelOptions }, localTransport] }; } + const pinoOptions: PinoOptions = { ...baseOptions, ...options }; - const logger = pino(pinoOptions, pinoTransport(transport) as DestinationStream); + const logger = pino(pinoOptions, pinoTransport(transportOptions) as DestinationStream); - if (options?.pinoCaller === true) { - return pinoCaller(logger); - } + const finalLogger = options?.pinoCaller === true ? pinoCaller(logger) : logger; + + finalLogger.debug({ + msg: 'logger initialized', + level: pinoOptions.level ?? 'info', + prettyPrint: prettyPrintEnabled, + pinoCaller: options?.pinoCaller === true, + otelEnabled, + otelUrl: otelEnabled ? otelUrl : undefined, + }); - return logger; + return finalLogger; } export type { Logger } from 'pino';