// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import pino from 'pino'; import { LRUCache } from 'lru-cache'; import type { LoggerType } from '../types/Logging'; import { Environment, getEnvironment } from '../environment'; import { reallyJsonStringify } from '../util/reallyJsonStringify'; import { getLogLevelString, type LogLevel } from './shared'; // This file is imported by some components so we can't import `ts/util/privacy` let redactAll = (value: string) => value; let destination: pino.DestinationStream | undefined; let buffer = new Array(); const COLORS = [ '#2c6bed', '#cf163e', '#c73f0a', '#6f6a58', '#3b7845', '#1d8663', '#077d92', '#336ba3', '#6058ca', '#9932c8', '#aa377a', '#8f616a', '#71717f', '#ebeae8', '#506ecd', '#ff9500', ]; const SUBSYSTEM_COLORS = new LRUCache({ max: 500, }); // Only for unpackaged app function getSubsystemColor(name: string): string { const cached = SUBSYSTEM_COLORS.get(name); if (cached != null) { return cached; } // Jenkins hash let hash = 0; /* eslint-disable no-bitwise */ for (let i = 0; i < name.length; i += 1) { hash += name.charCodeAt(i) & 0xff; hash += hash << 10; hash ^= hash >>> 6; } hash += hash << 3; hash ^= hash >>> 11; hash += hash << 15; hash >>>= 0; /* eslint-enable no-bitwise */ const result = COLORS[hash % COLORS.length]; SUBSYSTEM_COLORS.set(name, result); return result; } let cachedPattern: RegExp | undefined; if (typeof window !== 'undefined' && window.localStorage) { window.addEventListener('storage', event => { if (event.key === 'debug') { cachedPattern = undefined; } }); } function getPattern(): RegExp { if (cachedPattern != null) { return cachedPattern; } let value = ''; if (typeof window !== 'undefined' && window.localStorage) { value = window.localStorage.getItem('debug') || ''; } if (typeof process !== 'undefined' && process.env) { value = value || process.env.DEBUG || ''; } const parts = value .trim() .replace(/\s+/g, ',') .split(',') .filter(part => part) .map(part => { const result = part .replace(/([^a-zA-Z0-9_\s*])/g, '\\$1') .replace('*', '.*'); // Wrap with `[]` if not provided if (!/^\[.*\]/.test(part)) { return `\\[${result}\\]`; } return result; }); if (parts.length === 0) { return /^.*$/; } const result = new RegExp(`^(${parts.join('|')})\\s+$`); cachedPattern = result; return result; } function debugLog( logger: typeof pinoInstance, args: Array, level: LogLevel ): void { if (getEnvironment() === Environment.PackagedApp) { return; } const consoleMethod = getLogLevelString(level); const { msgPrefixSym } = pino.symbols as unknown as { readonly msgPrefixSym: unique symbol; }; const msgPrefix = (logger as unknown as Record)[ msgPrefixSym ]; const pattern = getPattern(); if (!pattern.test(msgPrefix ?? '')) { return; } const [message, ...extra] = args; const color = getSubsystemColor(msgPrefix ?? ''); // `fatal` has no respective analog in `console` // eslint-disable-next-line no-console console[consoleMethod === 'fatal' ? 'error' : consoleMethod]( `%c${msgPrefix ?? ''}%c${message}`, `color: ${color}; font-weight: bold`, 'color: inherit; font-weight: inherit', ...extra ); } const pinoInstance = pino( { formatters: { // No point in saving pid or hostname bindings: () => ({}), }, hooks: { logMethod(args, method, level) { debugLog(this, args, level); // Always call original method, but with stringified arguments for // compatibility with existing logging. // // (Since pino >= 6 extra arguments that don't correspond to %d/%s/%j // templates in the `message` are ignored) const line = args .map(item => typeof item === 'string' ? item : reallyJsonStringify(item) ) .join(' '); return method.call(this, line); }, }, timestamp: pino.stdTimeFunctions.isoTime, redact: { paths: ['*'], censor: item => redactAll(item), }, }, { write(msg) { if (destination == null) { buffer.push(msg); } else { destination.write(msg); } }, } ); export const log: LoggerType = { fatal: pinoInstance.fatal.bind(pinoInstance), error: pinoInstance.error.bind(pinoInstance), warn: pinoInstance.error.bind(pinoInstance), info: pinoInstance.info.bind(pinoInstance), debug: pinoInstance.debug.bind(pinoInstance), trace: pinoInstance.trace.bind(pinoInstance), child: child.bind(pinoInstance), }; function child(this: typeof pinoInstance, name: string): LoggerType { const instance = this.child({}, { msgPrefix: `[${name}] ` }); return { fatal: instance.fatal.bind(instance), error: instance.error.bind(instance), warn: instance.warn.bind(instance), info: instance.info.bind(instance), debug: instance.debug.bind(instance), trace: instance.trace.bind(instance), child: child.bind(instance), }; } export const createLogger = log.child; /** * Sets the low-level logging interface. Should be called early in a process's * life. */ export function setPinoDestination( newDestination: pino.DestinationStream, newRedactAll: typeof redactAll ): void { destination = newDestination; redactAll = newRedactAll; const queued = buffer; buffer = []; for (const msg of queued) { destination.write(msg); } }