/* oxlint-disable typescript/no-explicit-any */ import { AsyncLocalStorage } from 'node:async_hooks' import { context, trace } from '@opentelemetry/api' import lodash from 'lodash' import pino, { DestinationStream, Logger as PinoLogger, stdSerializers } from 'pino' import { AlsData, LogData, LogLevel, Logger, LoggerOptions } from '@diia-inhouse/types' import type * as Utils from '@diia-inhouse/utils' import { toInternalLoggerOptions } from './config.js' import { trimmer } from './trimmer.js' // oxlint-disable-next-line typescript/unbound-method, @diia-inhouse/class/no-module-level-const const { isObject } = lodash export default class DiiaLogger implements Logger { private logger: PinoLogger<'io'> private trim: ReturnType constructor( private readonly options: LoggerOptions | undefined = {}, private readonly asyncLocalStorage: AsyncLocalStorage | undefined = undefined, destinationStream: DestinationStream | null = null, existedLogger: PinoLogger<'io'> | null = null, ) { const internalOptions = toInternalLoggerOptions(this.options) const { redact: { paths: redactPaths }, } = internalOptions this.logger = existedLogger || pino( { timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`, level: this.options.logLevel ?? LogLevel.INFO, base: undefined, formatters: { level: (l) => ({ level: l.toUpperCase() }), }, mixin: () => { const logData = this.asyncLocalStorage?.getStore()?.logData const spanId = trace.getSpan(context.active())?.spanContext().spanId return { headers: spanId ? { ...logData, spanId } : logData, serviceVersion: this.getServiceVersion(), } }, serializers: { err: (value) => { const res = stdSerializers.err(value) if (!isObject(res)) { return res } res.constructorName = res.type res.type = value.type return res }, }, customLevels: { [LogLevel.IO]: 25, }, redact: [...(redactPaths || [])], }, destinationStream || undefined, ) this.trim = trimmer(internalOptions) } child(bindings: Record, destinationStream?: DestinationStream): Logger { const pinoLogger = this.logger.child(bindings) return new DiiaLogger(this.options, this.asyncLocalStorage, destinationStream, pinoLogger) } log(message: string, data: unknown = {}): void { this.printMessage('info', message, data) } info(message: string, data: unknown = {}): void { this.printMessage('info', message, data) } io(message: string, data: unknown = {}): void { this.printMessage('io' as pino.Level, message, data) } error(message: string, data: unknown = {}): void { this.printMessage('error', message, data) } warn(message: string, data: unknown = {}): void { this.printMessage('warn', message, data) } fatal(message: string, data: unknown = {}): void { this.printMessage('fatal', message, data) } trace(message: string, data: unknown = {}): void { this.printMessage('trace', message, data) } debug(message: string, data: unknown = {}): void { this.printMessage('debug', message, data) } prepareContext(data: LogData): LogData { return this.trim(data) } private printMessage(level: pino.Level, message: string, data: any): void { const logData = { log: data } const trimmedData = this.trim(logData) this.logger[level](this.extractPredefinedProperties(trimmedData), message) } private extractPredefinedProperties(data: any): any { if (data.log?.analytics) { const { analytics, ...rest } = data.log return { analytics, log: rest } } if (data.log?.err) { const { err, ...rest } = data.log return { err, log: rest } } if (data.log instanceof Error) { return { err: data.data } } return data } private getServiceVersion(): string | undefined { try { const { utils } = require('@diia-inhouse/utils') as typeof Utils return utils.getServiceVersion() } catch { return undefined } } } export { DiiaLogger } export { defaultOptions } from './config.js' export { createTrimmer } from './trimmer.js'