From f09ddc44933156ca7f6b67b4c93e41c15c31c3bb Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Thu, 18 Jan 2024 13:52:04 +0100 Subject: [PATCH] feat: add system logger (#457) Adds a system logger for internal usage, closely modeled after the edge-functions one. --- src/internal.ts | 5 +++ src/lib/system_logger.ts | 79 ++++++++++++++++++++++++++++++++++++++ test/unit/system_logger.js | 37 ++++++++++++++++++ tsconfig.json | 2 +- 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/internal.ts create mode 100644 src/lib/system_logger.ts create mode 100644 test/unit/system_logger.js diff --git a/src/internal.ts b/src/internal.ts new file mode 100644 index 00000000..14ce81c0 --- /dev/null +++ b/src/internal.ts @@ -0,0 +1,5 @@ +// this file is exported as @netlify/functions/internal, +// 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' diff --git a/src/lib/system_logger.ts b/src/lib/system_logger.ts new file mode 100644 index 00000000..a6cc5046 --- /dev/null +++ b/src/lib/system_logger.ts @@ -0,0 +1,79 @@ +const systemLogTag = '__nfSystemLog' + +const serializeError = (error: Error): Record => { + const cause = error?.cause instanceof Error ? serializeError(error.cause) : error.cause + + return { + error: error.message, + error_cause: cause, + error_stack: error.stack, + } +} + +// eslint pretends there's a different enum at the same place - it's wrong! +// eslint-disable-next-line no-shadow +export enum LogLevel { + Debug = 1, + Log, + Error, +} + +class SystemLogger { + private readonly fields: Record + private readonly logLevel: LogLevel + + constructor(fields: Record = {}, logLevel = LogLevel.Log) { + this.fields = fields + this.logLevel = logLevel + } + + private doLog(logger: typeof console.log, message: string) { + logger(systemLogTag, JSON.stringify({ msg: message, fields: this.fields })) + } + + log(message: string) { + if (this.logLevel > LogLevel.Log) { + return + } + + this.doLog(console.log, message) + } + + debug(message: string) { + if (this.logLevel > LogLevel.Debug) { + return + } + + this.doLog(console.debug, message) + } + + error(message: string) { + if (this.logLevel > LogLevel.Error) { + return + } + + this.doLog(console.error, message) + } + + withLogLevel(level: LogLevel) { + return new SystemLogger(this.fields, level) + } + + withFields(fields: Record) { + return new SystemLogger( + { + ...this.fields, + ...fields, + }, + this.logLevel, + ) + } + + withError(error: unknown) { + const fields = error instanceof Error ? serializeError(error) : { error } + + return this.withFields(fields) + } +} + +export const systemLogger = new SystemLogger() diff --git a/test/unit/system_logger.js b/test/unit/system_logger.js new file mode 100644 index 00000000..2a1926c7 --- /dev/null +++ b/test/unit/system_logger.js @@ -0,0 +1,37 @@ +const test = require('ava') + +const { systemLogger, LogLevel } = require('../../dist/internal') + +test('Log Level', (t) => { + const originalDebug = console.debug + + const debugLogs = [] + console.debug = (...message) => debugLogs.push(message) + + systemLogger.debug('hello!') + t.is(debugLogs.length, 0) + + systemLogger.withLogLevel(LogLevel.Debug).debug('hello!') + t.is(debugLogs.length, 1) + + systemLogger.withLogLevel(LogLevel.Log).debug('hello!') + t.is(debugLogs.length, 1) + + console.debug = originalDebug +}) + +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!') + t.is(logs.length, 1) + t.is(logs[0][0], '__nfSystemLog') + const log = JSON.parse(logs[0][1]) + t.is(log.msg, 'hello!') + 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 +}) diff --git a/tsconfig.json b/tsconfig.json index e07baa7e..2fb9dfec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */