diff --git a/src/internal.ts b/src/internal.ts index 14ce81c0..c9b47263 100644 --- a/src/internal.ts +++ b/src/internal.ts @@ -2,4 +2,4 @@ // and only meant for consumption by Netlify Teams. // While we try to adhere to semver, this file is not considered part of the public API. -export { systemLogger, LogLevel } from './lib/system_logger.js' +export { systemLogger, LogLevel, StructuredLogger } from './lib/system_logger.js' diff --git a/src/lib/system_logger.ts b/src/lib/system_logger.ts index 80afab13..d937afd6 100644 --- a/src/lib/system_logger.ts +++ b/src/lib/system_logger.ts @@ -1,5 +1,3 @@ -import { env } from 'process' - const systemLogTag = '__nfSystemLog' const serializeError = (error: Error): Record => { @@ -18,23 +16,26 @@ export enum LogLevel { Debug = 1, Log, Error, + Silent = Number.POSITIVE_INFINITY, } -class SystemLogger { +type RawLogger = (...data: unknown[]) => void + +export class StructuredLogger { private readonly fields: Record private readonly logLevel: LogLevel + private readonly rawLogger?: RawLogger - constructor(fields: Record = {}, logLevel = LogLevel.Log) { + constructor(logLevel: LogLevel, rawLogger?: RawLogger, fields: Record = {}) { this.fields = fields this.logLevel = logLevel + this.rawLogger = rawLogger } - private doLog(logger: typeof console.log, message: string) { - if (env.NETLIFY_DEV && !env.NETLIFY_ENABLE_SYSTEM_LOGGING) { - return - } + private doLog(message: string, level: string, defaultLogger: RawLogger) { + const logger = this.rawLogger ?? defaultLogger - logger(systemLogTag, JSON.stringify({ msg: message, fields: this.fields })) + logger(systemLogTag, JSON.stringify({ msg: message, fields: this.fields, level })) } log(message: string) { @@ -42,7 +43,7 @@ class SystemLogger { return } - this.doLog(console.log, message) + this.doLog(message, 'log', console.log) } debug(message: string) { @@ -50,7 +51,7 @@ class SystemLogger { return } - this.doLog(console.debug, message) + this.doLog(message, 'debug', console.debug) } error(message: string) { @@ -58,21 +59,18 @@ class SystemLogger { return } - this.doLog(console.error, message) + this.doLog(message, 'error', console.error) } withLogLevel(level: LogLevel) { - return new SystemLogger(this.fields, level) + return new StructuredLogger(level, this.rawLogger, this.fields) } withFields(fields: Record) { - return new SystemLogger( - { - ...this.fields, - ...fields, - }, - this.logLevel, - ) + return new StructuredLogger(this.logLevel, this.rawLogger, { + ...this.fields, + ...fields, + }) } withError(error: unknown) { @@ -82,4 +80,4 @@ class SystemLogger { } } -export const systemLogger = new SystemLogger() +export const systemLogger = new StructuredLogger(LogLevel.Log) diff --git a/test/unit/system_logger.js b/test/unit/system_logger.js index 712dfb28..f0f62fb1 100644 --- a/test/unit/system_logger.js +++ b/test/unit/system_logger.js @@ -1,29 +1,44 @@ -const process = require("process") - const test = require('ava') -const { systemLogger, LogLevel } = require('../../dist/internal') +const { systemLogger, LogLevel, StructuredLogger } = require('../../dist/internal') + +const consoleDebug = console.debug +const consoleError = console.error +const consoleLog = console.log + +test.afterEach(() => { + console.debug = consoleDebug + console.error = consoleError + console.log = consoleLog +}) -test('Log Level', (t) => { - const originalDebug = console.debug +test('Log levels', (t) => { + const logs = { + debug: [], + error: [], + log: [], + } + console.debug = (...message) => logs.debug.push(message) + console.error = (...message) => logs.error.push(message) + console.log = (...message) => logs.log.push(message) - const debugLogs = [] - console.debug = (...message) => debugLogs.push(message) + systemLogger.debug('debug 1') + t.is(logs.debug.length, 0) - systemLogger.debug('hello!') - t.is(debugLogs.length, 0) + systemLogger.log('log 1') + t.is(logs.log.length, 1) - systemLogger.withLogLevel(LogLevel.Debug).debug('hello!') - t.is(debugLogs.length, 1) + systemLogger.withLogLevel(LogLevel.Debug).debug('debug 2') + t.is(logs.debug.length, 1) - systemLogger.withLogLevel(LogLevel.Log).debug('hello!') - t.is(debugLogs.length, 1) + systemLogger.withLogLevel(LogLevel.Debug).error('error 1') + t.is(logs.error.length, 1) - console.debug = originalDebug + systemLogger.withLogLevel(LogLevel.Silent).error('error 2') + t.is(logs.error.length, 1) }) test('Fields', (t) => { - const originalLog = console.log const logs = [] console.log = (...message) => logs.push(message) systemLogger.withError(new Error('boom')).withFields({ foo: 'bar' }).log('hello!') @@ -34,26 +49,49 @@ test('Fields', (t) => { t.is(log.fields.foo, 'bar') t.is(log.fields.error, 'boom') t.is(log.fields.error_stack.split('\n').length > 2, true) - - console.log = originalLog + t.is(log.level, 'log') }) -test('Local Dev', (t) => { - const originalLog = console.log - const logs = [] - console.log = (...message) => logs.push(message) - systemLogger.log('hello!') - t.is(logs.length, 1) +test('Accepts a custom raw logger', (t) => { + const logs = { + debug: [], + error: [], + log: [], + } + console.debug = () => { + throw new Error('Unexpected `console.debug` call') + } + console.error = () => { + throw new Error('Unexpected `console.error` call') + } + console.log = () => { + throw new Error('Unexpected `console.log` call') + } + const rawLogger = (tag, payload) => { + t.is(tag, '__nfSystemLog') - process.env.NETLIFY_DEV= "true" - systemLogger.log('hello!') - t.is(logs.length, 1) + const { msg, fields, level } = JSON.parse(payload) + const bucket = logs[level] + + t.truthy(bucket) + + bucket.push({ fields, msg }) + } + + const logger = new StructuredLogger(LogLevel.Log, rawLogger, {}) + + logger.debug('debug 1') + t.is(logs.debug.length, 0) + + logger.log('log 1') + t.is(logs.log.length, 1) + + logger.withLogLevel(LogLevel.Debug).debug('debug 2') + t.is(logs.debug.length, 1) - process.env.NETLIFY_ENABLE_SYSTEM_LOGGING= "true" - systemLogger.log('hello!') - t.is(logs.length, 2) + logger.withLogLevel(LogLevel.Debug).error('error 1') + t.is(logs.error.length, 1) - delete process.env.NETLIFY_DEV - delete process.env.NETLIFY_ENABLE_SYSTEM_LOGGING - console.log = originalLog + logger.withLogLevel(LogLevel.Silent).error('error 2') + t.is(logs.error.length, 1) })