From 45d8812a3b8fb1b145db057565dee7321cd2510a Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Mon, 19 Jan 2026 16:01:40 +0100 Subject: [PATCH 01/29] feat(create-json-log-object-data): improved handling of logged error instances BREAKING CHANGE: - changed structure for JsonLogObjectData --- packages/logger/package.json | 14 +- .../logger/src/model/json-log-object-data.ts | 43 ------ .../logger/src/model/log-transport.spec.ts | 4 +- packages/logger/src/model/logger.spec.ts | 5 +- .../logger/src/{index.ts => public-api.ts} | 6 +- .../src/services/base-logger.service.spec.ts | 2 +- packages/logger/src/testing/public-api.ts | 1 + .../testing}/spy-log.transport.ts | 5 +- .../create-json-log-object-data.function.ts | 55 ++++++++ .../logger/src/utils/format-error.function.ts | 72 ++++++++++ ...t-json-stringify-replacer.function.spec.ts | 129 ++++++++++++++++++ .../get-json-stringify-replacer.function.ts | 31 +++++ .../json-log-object-data.spec.ts | 41 +++++- .../{model => utils}/json-log-transport.ts | 6 +- packages/logger/test/console-mock.function.ts | 25 ++++ packages/logger/tsconfig.json | 1 + 16 files changed, 375 insertions(+), 65 deletions(-) delete mode 100644 packages/logger/src/model/json-log-object-data.ts rename packages/logger/src/{index.ts => public-api.ts} (50%) create mode 100644 packages/logger/src/testing/public-api.ts rename packages/logger/{test => src/testing}/spy-log.transport.ts (73%) create mode 100644 packages/logger/src/utils/create-json-log-object-data.function.ts create mode 100644 packages/logger/src/utils/format-error.function.ts create mode 100644 packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts create mode 100644 packages/logger/src/utils/get-json-stringify-replacer.function.ts rename packages/logger/src/{model => utils}/json-log-object-data.spec.ts (50%) rename packages/logger/src/{model => utils}/json-log-transport.ts (69%) create mode 100644 packages/logger/test/console-mock.function.ts diff --git a/packages/logger/package.json b/packages/logger/package.json index b5f68c9..76f98c9 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -9,12 +9,12 @@ "type": "module", "exports": { ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" + "types": "./dist/public-api.d.ts", + "default": "./dist/public-api.js" }, - "./test/*.js": { - "types": "./dist/test/*.d.ts", - "default": "./dist/test/*.js" + "./testing": { + "types": "./dist/testing/public-api.d.ts", + "default": "./dist/testing/public-api.js" } }, "scripts": { @@ -36,10 +36,10 @@ "@shiftcode/utilities": "^4.3.1" }, "peerDependencies": { - "@shiftcode/utilities": "^4.0.0 || ^4.0.0-pr53" + "@shiftcode/utilities": "^4.0.0" }, "engines": { - "node": "^20.0.0 || ^22.0.0" + "node": "^22.0.0 || ^24.0.0" }, "publishConfig": { "directory": "dist" diff --git a/packages/logger/src/model/json-log-object-data.ts b/packages/logger/src/model/json-log-object-data.ts deleted file mode 100644 index 38ade41..0000000 --- a/packages/logger/src/model/json-log-object-data.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { getEnumKeyFromNum } from '@shiftcode/utilities' - -import { LogLevel } from './log-level.enum.js' - -export interface JsonLogObjectData { - level: string - logger: string - timestamp: string /* ISO string */ - message?: string - errorName?: string - exception?: string - data?: any[] -} - -export function createJsonLogObjectData( - level: LogLevel, - clazzName: string, - timestamp: Date, - args: any[], -): JsonLogObjectData { - const logObjectData: Partial = { - level: getEnumKeyFromNum(LogLevel, level), - timestamp: timestamp.toISOString(), - logger: clazzName, - } - - const msgOrError = args.shift() - if (typeof msgOrError === 'string') { - logObjectData.message = msgOrError - } else if (msgOrError instanceof Error) { - logObjectData.message = msgOrError.message - logObjectData.errorName = msgOrError.name - logObjectData.exception = msgOrError.stack?.toString() - } else { - // first param is neither string nor error, put it back to args to pass it to data - args = [msgOrError, ...args] - } - if (args.length) { - logObjectData.data = args.length === 1 ? args[0] : args - } - - return logObjectData as JsonLogObjectData -} diff --git a/packages/logger/src/model/log-transport.spec.ts b/packages/logger/src/model/log-transport.spec.ts index 3b0944c..b99856b 100644 --- a/packages/logger/src/model/log-transport.spec.ts +++ b/packages/logger/src/model/log-transport.spec.ts @@ -1,6 +1,6 @@ -import { SpyLogTransport } from '../../test/spy-log.transport.js' -import { LogLevel } from '../index.js' +import { SpyLogTransport } from '../testing/spy-log.transport.js' import { stringToColor } from '../utils/logger-helper.js' +import { LogLevel } from './log-level.enum.js' describe('respects the configured level', () => { test('respects level DEBUG', () => { diff --git a/packages/logger/src/model/logger.spec.ts b/packages/logger/src/model/logger.spec.ts index 345fc03..95bc603 100644 --- a/packages/logger/src/model/logger.spec.ts +++ b/packages/logger/src/model/logger.spec.ts @@ -1,6 +1,7 @@ -import { SpyLogTransport } from '../../test/spy-log.transport.js' -import { Logger, LogLevel } from '../index.js' +import { SpyLogTransport } from '../testing/spy-log.transport.js' import { stringToColor } from '../utils/logger-helper.js' +import { LogLevel } from './log-level.enum.js' +import { Logger } from './logger.js' describe('Logger behavior', () => { let logger: Logger diff --git a/packages/logger/src/index.ts b/packages/logger/src/public-api.ts similarity index 50% rename from packages/logger/src/index.ts rename to packages/logger/src/public-api.ts index 55ed705..ff7cc78 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/public-api.ts @@ -1,7 +1,9 @@ -export * from './model/json-log-object-data.js' -export * from './model/json-log-transport.js' export * from './model/log-level.enum.js' export * from './model/log-transport.js' export * from './model/logger.js' export * from './services/base-logger.service.js' +export * from './utils/create-json-log-object-data.function.js' +export * from './utils/format-error.function.js' +export * from './utils/format-error.function.js' +export * from './utils/json-log-transport.js' export * from './utils/logger-helper.js' diff --git a/packages/logger/src/services/base-logger.service.spec.ts b/packages/logger/src/services/base-logger.service.spec.ts index c4943c8..0985dd6 100644 --- a/packages/logger/src/services/base-logger.service.spec.ts +++ b/packages/logger/src/services/base-logger.service.spec.ts @@ -1,6 +1,6 @@ -import { SpyLogTransport } from '../../test/spy-log.transport.js' import { LogLevel } from '../model/log-level.enum.js' import { Logger } from '../model/logger.js' +import { SpyLogTransport } from '../testing/spy-log.transport.js' import { BaseLoggerService } from './base-logger.service.js' describe('BaseLoggerService with SpyLogTransport', () => { diff --git a/packages/logger/src/testing/public-api.ts b/packages/logger/src/testing/public-api.ts new file mode 100644 index 0000000..a8ef48c --- /dev/null +++ b/packages/logger/src/testing/public-api.ts @@ -0,0 +1 @@ +export * from './spy-log.transport.js' diff --git a/packages/logger/test/spy-log.transport.ts b/packages/logger/src/testing/spy-log.transport.ts similarity index 73% rename from packages/logger/test/spy-log.transport.ts rename to packages/logger/src/testing/spy-log.transport.ts index 30fbd5a..f85f680 100644 --- a/packages/logger/test/spy-log.transport.ts +++ b/packages/logger/src/testing/spy-log.transport.ts @@ -1,6 +1,9 @@ +// eslint-disable-next-line import/no-extraneous-dependencies import { jest } from '@jest/globals' -import { LogLevel, LogTransport } from '../src/index.js' +import { LogLevel } from '../model/log-level.enum.js' +import { LogTransport } from '../model/log-transport.js' + export class SpyLogTransport extends LogTransport { private logMock = jest.fn() diff --git a/packages/logger/src/utils/create-json-log-object-data.function.ts b/packages/logger/src/utils/create-json-log-object-data.function.ts new file mode 100644 index 0000000..ed93229 --- /dev/null +++ b/packages/logger/src/utils/create-json-log-object-data.function.ts @@ -0,0 +1,55 @@ +import { getEnumKeyFromNum } from '@shiftcode/utilities' + +import { LogLevel } from '../model/log-level.enum.js' +import { ErrorAttributes, formatError } from './format-error.function.js' + +// used to enforce the usage of the factory function without any runtime overhead +// therefore not intended to be used in runtime code +declare const JSON_LOG_OBJECT_DATA_BRAND: unique symbol; + + +/** + * make sure to use the {@link createJsonLogObjectData} util function to create an instance of this interface + */ +export interface JsonLogObjectData { + [JSON_LOG_OBJECT_DATA_BRAND]: true; + + level: string + logger: string + timestamp: string /* ISO string */ + + message?: string + error?: ErrorAttributes + + data?: unknown +} + +export function createJsonLogObjectData(level: LogLevel, clazzName: string, timestamp: Date, args: unknown[]): JsonLogObjectData { + const logObjectData: Partial = { + level: getEnumKeyFromNum(LogLevel, level), + timestamp: timestamp.toISOString(), + logger: clazzName, + } + + // if first arg is string, it's the message + if (typeof args[0] === 'string') { + logObjectData.message = args.shift() as string + } + + // if any arg is Error, extract it (only first one) + // --> if no specific message, use error message as log message + const errIndex = args.findIndex((arg) => arg instanceof Error) + if (errIndex !== -1) { + logObjectData.error = formatError(args.splice(errIndex, 1)[0] as Error) + if (!logObjectData.message) { + logObjectData.message = logObjectData.error.message + } + } + + // remaining args go to data + if (args.length) { + logObjectData.data = args.length === 1 ? args[0] : args + } + + return logObjectData as JsonLogObjectData +} diff --git a/packages/logger/src/utils/format-error.function.ts b/packages/logger/src/utils/format-error.function.ts new file mode 100644 index 0000000..4b0de7b --- /dev/null +++ b/packages/logger/src/utils/format-error.function.ts @@ -0,0 +1,72 @@ +export interface ErrorAttributes { + [key: string]: unknown + + name: string + location: string + message: string + stack?: string + cause?: unknown +} + +/** + * Format an error into a loggable object. + * + * @example + * ```json + * { + * "name": "Error", + * "location": "file.js:1", + * "message": "An error occurred", + * "stack": "Error: An error occurred\n at file.js:1\n at file.js:2\n at file.js:3", + * "cause": { + * "name": "OtherError", + * "location": "file.js:2", + * "message": "Another error occurred", + * "stack": "Error: Another error occurred\n at file.js:2\n at file.js:3\n at file.js:4" + * } + * } + * ``` + * + * @param error - Error to format + */ +export function formatError(error: Error): ErrorAttributes { + const { name, message, stack, cause, ...errorAttributes } = error + const formattedError: ErrorAttributes = { + name, + location: getCodeLocation(error.stack), + message, + stack, + cause: cause instanceof Error ? formatError(cause) : cause, + } + for (const key in error) { + if (typeof key === 'string' && !['name', 'message', 'stack', 'cause'].includes(key)) { + formattedError[key] = (errorAttributes as Record)[key] + } + } + + return formattedError +} + +/** + * Get the location of an error from a stack trace. + * + * @param stack - stack trace to parse + */ +function getCodeLocation(stack?: string): string { + if (!stack) { + return '' + } + + const stackLines = stack.split('\n') + const regex = /\(([^()]*?):(\d+?):(\d+?)\)\\?$/ + + for (const item of stackLines) { + const match = regex.exec(item) + + if (Array.isArray(match)) { + return `${match[1]}:${Number(match[2])}` + } + } + + return '' +} diff --git a/packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts b/packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts new file mode 100644 index 0000000..dc07676 --- /dev/null +++ b/packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts @@ -0,0 +1,129 @@ +import { expect } from '@jest/globals' + +import { getJsonStringifyReplacer } from './get-json-stringify-replacer.function.js' + +describe('getJsonStringifyReplacer', () => { + it('should handle BigInt values', () => { + const replacer = getJsonStringifyReplacer() + const result = replacer('key', 123456789123456789n) + + expect(result).toBe('123456789123456789') + }) + + it('should format Error instances', () => { + const error = new Error('Test error') + const replacer = getJsonStringifyReplacer() + const result = replacer('key', error) + + expect(result).toEqual({ + name: 'Error', + location: expect.any(String), + message: 'Test error', + stack: expect.any(String), + cause: undefined, + }) + }) + + it('should handle circular references', () => { + const obj: any = { name: 'test' } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + obj.self = obj + + const replacer = getJsonStringifyReplacer() + + // First call should return the object + const firstResult = replacer('obj', obj) + expect(firstResult).toBe(obj) + + // Second call with same object should return circular reference marker + const secondResult = replacer('self', obj) + expect(secondResult).toBe('') + }) + + it('should not alter null values', () => { + const replacer = getJsonStringifyReplacer() + const result = replacer('key', null) + + expect(result).toBe(null) + }) + + it('should not alter primitive values', () => { + const replacer = getJsonStringifyReplacer() + + expect(replacer('key', 'string')).toBe('string') + expect(replacer('key', 42)).toBe(42) + expect(replacer('key', true)).toBe(true) + expect(replacer('key', undefined)).toBe(undefined) + }) + + it('should not alter simple arrays', () => { + const arr = [1, 2, 3] + const replacer = getJsonStringifyReplacer() + const result = replacer('arr', arr) + expect(result).toBe(arr) + }) + + it('should apply custom replacer', () => { + const customReplacer = (key: string, value: unknown) => { + if (key === 'secret') { + return '***' + } + return value + } + + const replacer = getJsonStringifyReplacer(customReplacer) + + expect(replacer('secret', 'password123')).toBe('***') + }) + + it('should use custom replacer prior handling BigInt values', () => { + const doubleBigInts = (key: string, value: unknown) => { + if (typeof value === 'bigint') { + return value * 2n + } + return value + } + + const replacer = getJsonStringifyReplacer(doubleBigInts) + const result = replacer('key', 50n) + expect(result).toBe('100') + }) + + it('should use custom replacer prior handling Error instances', () => { + const wrapErrors = (key: string, value: unknown) => { + if (value instanceof Error) { + return new Error('Failed', {cause: value}) + } + return value + } + + const replacer = getJsonStringifyReplacer(wrapErrors) + const result = replacer('key', new Error('Original error')) + + expect(result).toHaveProperty('message', 'Failed') + expect(result).toHaveProperty('cause', expect.objectContaining({ message: 'Original error' })) + }) + + it('should work with JSON.stringify', () => { + const obj: any = { + name: 'test', + symbol: Symbol('test'), + bigNumber: 9007199254740991n, + error: new Error('Test error'), + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + obj.circular = obj + + const handleSymbols = (_: string, value: unknown) => (typeof value === 'symbol' ? value.toString() : value) + + const replacer = getJsonStringifyReplacer(handleSymbols) + const result = JSON.stringify(obj, replacer) + + expect(result).toContain('"name":"test"') + expect(result).toContain('"symbol":"Symbol(test)"') + expect(result).toContain('"bigNumber":"9007199254740991"') + expect(result).toContain('"circular":""') + }) + +}) + diff --git a/packages/logger/src/utils/get-json-stringify-replacer.function.ts b/packages/logger/src/utils/get-json-stringify-replacer.function.ts new file mode 100644 index 0000000..f643cd9 --- /dev/null +++ b/packages/logger/src/utils/get-json-stringify-replacer.function.ts @@ -0,0 +1,31 @@ +import { formatError } from './format-error.function.js' + +type JsonStringifyReplacer = (key: string, value: unknown) => unknown + +/** + * handles circular references & bigints and formats errors + * @param jsonStringifyReplacer - custom replacer function which is called prior to the built-in handling + */ +export function getJsonStringifyReplacer(jsonStringifyReplacer?: JsonStringifyReplacer): JsonStringifyReplacer { + const references = new WeakSet() + + return (key, value) => { + let replacedValue = jsonStringifyReplacer ? jsonStringifyReplacer(key, value) : value + + if (typeof replacedValue === 'bigint') { + return replacedValue.toString() + } else if (replacedValue instanceof Error) { + replacedValue = formatError(replacedValue) + } + + if (typeof replacedValue === 'object' && replacedValue !== null) { + if (references.has(replacedValue)) { + return `` + } else { + references.add(replacedValue) + } + } + + return replacedValue + } +} diff --git a/packages/logger/src/model/json-log-object-data.spec.ts b/packages/logger/src/utils/json-log-object-data.spec.ts similarity index 50% rename from packages/logger/src/model/json-log-object-data.spec.ts rename to packages/logger/src/utils/json-log-object-data.spec.ts index c462b9c..6f4073b 100644 --- a/packages/logger/src/model/json-log-object-data.spec.ts +++ b/packages/logger/src/utils/json-log-object-data.spec.ts @@ -1,5 +1,7 @@ -import { createJsonLogObjectData } from './json-log-object-data.js' -import { LogLevel } from './log-level.enum.js' +import { expect } from '@jest/globals' + +import { LogLevel } from '../model/log-level.enum.js' +import { createJsonLogObjectData } from './create-json-log-object-data.function.js' describe('createJsonLogObjectData', () => { it('should create a log object with a message', () => { @@ -17,6 +19,8 @@ describe('createJsonLogObjectData', () => { it('should create a log object with an error', () => { const error = new Error('Test error') + error.name = 'TestError' + const result = createJsonLogObjectData(LogLevel.ERROR, 'MyClass', new Date('2023-01-01T00:00:00.000Z'), [error]) expect(result).toEqual({ @@ -24,8 +28,37 @@ describe('createJsonLogObjectData', () => { logger: 'MyClass', timestamp: '2023-01-01T00:00:00.000Z', message: 'Test error', - errorName: 'Error', - exception: error.stack, + error: { + name: 'TestError', + message: 'Test error', + cause: undefined, + location: expect.stringContaining('json-log-object-data.spec.ts'), + stack: expect.stringContaining(error.name), + }, + }) + }) + + it('should create a log object with an message and an error', () => { + const error = new Error('Test error') + error.name = 'TestError' + + const result = createJsonLogObjectData(LogLevel.ERROR, 'MyClass', new Date('2023-01-01T00:00:00.000Z'), [ + 'Something Failed', + error, + ]) + + expect(result).toEqual({ + level: 'ERROR', + logger: 'MyClass', + timestamp: '2023-01-01T00:00:00.000Z', + message: 'Something Failed', + error: { + name: 'TestError', + message: 'Test error', + cause: undefined, + location: expect.stringContaining('json-log-object-data.spec.ts'), + stack: expect.stringContaining(error.name), + }, }) }) diff --git a/packages/logger/src/model/json-log-transport.ts b/packages/logger/src/utils/json-log-transport.ts similarity index 69% rename from packages/logger/src/model/json-log-transport.ts rename to packages/logger/src/utils/json-log-transport.ts index ca3f24d..612d223 100644 --- a/packages/logger/src/model/json-log-transport.ts +++ b/packages/logger/src/utils/json-log-transport.ts @@ -1,6 +1,6 @@ -import { createJsonLogObjectData, JsonLogObjectData } from './json-log-object-data.js' -import { LogLevel } from './log-level.enum.js' -import { LogTransport } from './log-transport.js' +import { LogLevel } from '../model/log-level.enum.js' +import { LogTransport } from '../model/log-transport.js' +import { createJsonLogObjectData, JsonLogObjectData } from './create-json-log-object-data.function.js' export abstract class JsonLogTransport extends LogTransport { protected constructor(logLevel: LogLevel) { diff --git a/packages/logger/test/console-mock.function.ts b/packages/logger/test/console-mock.function.ts new file mode 100644 index 0000000..a17f668 --- /dev/null +++ b/packages/logger/test/console-mock.function.ts @@ -0,0 +1,25 @@ +import { jest } from '@jest/globals' + +const originalConsole = { ...console } + +export interface ConsoleMock { + debug: jest.Mock + info: jest.Mock + warn: jest.Mock + error: jest.Mock + log?: jest.Mock +} + +export function mockConsole() { + console.log = jest.fn() + console.debug = jest.fn() + console.info = jest.fn() + console.warn = jest.fn() + console.error = jest.fn() + + return (console) +} + +export function restoreConsole() { + console = originalConsole +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json index 1c18066..86dd6f1 100644 --- a/packages/logger/tsconfig.json +++ b/packages/logger/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { + "rootDir": "./src", "baseUrl": "./", "outDir": "./dist", "declarationDir": "./dist" From c5753820eb7471fe5ef7fbb9e1d0ad2334c5ffe0 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Mon, 19 Jan 2026 16:03:41 +0100 Subject: [PATCH 02/29] feat(console-json-log-transport): expose consoleJsonLogTransport --- packages/logger/src/public-api.ts | 2 + .../console-json-log-transport-config.ts | 6 + .../console-json-log.transport.spec.ts | 246 ++++++++++++++++++ .../console-json-log.transport.ts | 41 +++ 4 files changed, 295 insertions(+) create mode 100644 packages/logger/src/transports/console-json-log-transport/console-json-log-transport-config.ts create mode 100644 packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts create mode 100644 packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index ff7cc78..4281f08 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -2,6 +2,8 @@ export * from './model/log-level.enum.js' export * from './model/log-transport.js' export * from './model/logger.js' export * from './services/base-logger.service.js' +export * from './transports/console-json-log-transport/console-json-log.transport.js' +export * from './transports/console-json-log-transport/console-json-log-transport-config.js' export * from './utils/create-json-log-object-data.function.js' export * from './utils/format-error.function.js' export * from './utils/format-error.function.js' diff --git a/packages/logger/src/transports/console-json-log-transport/console-json-log-transport-config.ts b/packages/logger/src/transports/console-json-log-transport/console-json-log-transport-config.ts new file mode 100644 index 0000000..be25f25 --- /dev/null +++ b/packages/logger/src/transports/console-json-log-transport/console-json-log-transport-config.ts @@ -0,0 +1,6 @@ +import { LogLevel } from '../../model/log-level.enum.js' + +export interface ConsoleJsonLogTransportConfig { + logLevel: LogLevel + jsonStringifyReplacer?: (key: string, value: any) => any +} diff --git a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts b/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts new file mode 100644 index 0000000..f9d2594 --- /dev/null +++ b/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts @@ -0,0 +1,246 @@ +import { afterEach,beforeEach, describe, expect, test } from '@jest/globals' + +import { ConsoleMock, mockConsole, restoreConsole } from '../../../test/console-mock.function.js' +import { LogLevel } from '../../model/log-level.enum.js' +import { stringToColor } from '../../utils/logger-helper.js' +import { ConsoleJsonLogTransport } from './console-json-log.transport.js' + +describe('uses console statement according to levels', () => { + let logger: ConsoleJsonLogTransport + let logArgs: any[] + let timestamp: Date + let consoleMock: ConsoleMock + + beforeEach(() => { + consoleMock = mockConsole() + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.DEBUG }) + logArgs = ['foo bar'] + timestamp = new Date() + }) + afterEach(restoreConsole) + + test('calls correct console level for DEBUG', () => { + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), timestamp, logArgs) + expect(consoleMock.debug).toHaveBeenCalled() + expect(consoleMock.debug).toHaveBeenCalledWith( + JSON.stringify({ + level: 'DEBUG', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + message: 'foo bar', + }), + ) + }) + test('calls correct console level for INFO', () => { + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), timestamp, logArgs) + expect(consoleMock.info).toHaveBeenCalled() + expect(consoleMock.info).toHaveBeenCalledWith( + JSON.stringify({ + level: 'INFO', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + message: 'foo bar', + }), + ) + }) + test('calls correct console level for WARN', () => { + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), timestamp, logArgs) + expect(consoleMock.warn).toHaveBeenCalled() + expect(consoleMock.warn).toHaveBeenCalledWith( + JSON.stringify({ + level: 'WARN', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + message: 'foo bar', + }), + ) + }) + test('calls correct console level for ERROR', () => { + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), timestamp, logArgs) + expect(consoleMock.error).toHaveBeenCalled() + expect(consoleMock.error).toHaveBeenCalledWith( + JSON.stringify({ + level: 'ERROR', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + message: 'foo bar', + }), + ) + }) +}) + +describe('respects the configured level', () => { + let consoleMock: ConsoleMock + + beforeEach(() => { + consoleMock = mockConsole() + }) + afterEach(restoreConsole) + + test('respects level DEBUG', () => { + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.DEBUG }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(1) + expect(consoleMock.info).toHaveBeenCalledTimes(1) + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) + test('respects level INFO', () => { + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.INFO }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(0) + expect(consoleMock.info).toHaveBeenCalledTimes(1) + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) + test('respects level WARN', () => { + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.WARN }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(0) + expect(consoleMock.info).toHaveBeenCalledTimes(0) + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) + test('respects level ERROR', () => { + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.ERROR }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(0) + expect(consoleMock.info).toHaveBeenCalledTimes(0) + expect(consoleMock.warn).toHaveBeenCalledTimes(0) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) +}) + +describe('use first argument as message if string', () => { + let logger: ConsoleJsonLogTransport + let consoleMock: ConsoleMock + let timestamp: Date + + beforeEach(() => { + consoleMock = mockConsole() + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.DEBUG }) + timestamp = new Date() + }) + afterEach(restoreConsole) + + test('if only 1 arg', () => { + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), timestamp, ['foo bar debug']) + expect(consoleMock.debug).toHaveBeenCalledWith( + JSON.stringify({ + level: 'DEBUG', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + message: 'foo bar debug', + }), + ) + }) + test('if second arg is object, put it to data', () => { + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), timestamp, ['foo bar debug', { value: true }]) + expect(consoleMock.debug).toHaveBeenCalledWith( + JSON.stringify({ + level: 'DEBUG', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + message: 'foo bar debug', + data: { + value: true, + }, + }), + ) + }) + test('if several additional object args, put them to data', () => { + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), timestamp, [ + 'foo bar debug', + { first: true }, + { second: false }, + ]) + expect(consoleMock.debug).toHaveBeenCalledWith( + JSON.stringify({ + level: 'DEBUG', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + message: 'foo bar debug', + data: [{ first: true }, { second: false }], + }), + ) + }) + + test('if first is not a string, message property is undefined', () => { + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), timestamp, [{ name: 'foo bar' }]) + expect(consoleMock.debug).toHaveBeenCalledWith( + JSON.stringify({ + level: 'DEBUG', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + data: { name: 'foo bar' }, + }), + ) + }) +}) + +describe('handles circular references in log data', () => { + let logger: ConsoleJsonLogTransport + let consoleMock: ConsoleMock + let timestamp: Date + + beforeEach(() => { + consoleMock = mockConsole() + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.DEBUG }) + timestamp = new Date() + }) + afterEach(restoreConsole) + + test('in single object argument', () => { + const circularObj: any = { name: 'circular' } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + circularObj.self = circularObj + + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), timestamp, [circularObj]) + + expect(consoleMock.debug).toHaveBeenCalledWith( + JSON.stringify({ + level: 'DEBUG', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + data: { + name: 'circular', + self: '', + }, + }), + ) + }) + + test('in nested object argument', () => { + const circularObj: any = { name: 'circular' } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + circularObj.nested = { inner: circularObj } + + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), timestamp, [circularObj]) + + expect(consoleMock.debug).toHaveBeenCalledWith( + JSON.stringify({ + level: 'DEBUG', + timestamp: timestamp.toISOString(), + logger: 'MyClass', + data: { + name: 'circular', + nested: { + inner: '', + }, + }, + }), + ) + }) +}) diff --git a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts new file mode 100644 index 0000000..81a553d --- /dev/null +++ b/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts @@ -0,0 +1,41 @@ +import { jsonMapSetStringifyReplacer } from '@shiftcode/utilities' + +import { LogLevel } from '../../model/log-level.enum.js' +import { JsonLogObjectData } from '../../utils/create-json-log-object-data.function.js' +import { getJsonStringifyReplacer } from '../../utils/get-json-stringify-replacer.function.js' +import { JsonLogTransport } from '../../utils/json-log-transport.js' +import { ConsoleJsonLogTransportConfig } from './console-json-log-transport-config.js' + +export class ConsoleJsonLogTransport extends JsonLogTransport { + private readonly jsonStringifyReplacer: (key: string, value: any) => any + + constructor(config: ConsoleJsonLogTransportConfig) { + super(config.logLevel) + this.jsonStringifyReplacer = config.jsonStringifyReplacer ?? jsonMapSetStringifyReplacer + } + + transportLog(level: LogLevel, logDataObject: JsonLogObjectData): void { + const toLog = [JSON.stringify(logDataObject, getJsonStringifyReplacer(this.jsonStringifyReplacer))] + + /* eslint-disable prefer-spread,no-console */ + switch (level) { + case LogLevel.DEBUG: + console.debug.apply(console, toLog) + break + case LogLevel.ERROR: + console.error.apply(console, toLog) + break + case LogLevel.INFO: + console.info.apply(console, toLog) + break + case LogLevel.WARN: + console.warn.apply(console, toLog) + break + case LogLevel.OFF: + break + default: + return level // exhaustive check + } + /* eslint-enable prefer-spread,no-console */ + } +} From ac173c3d5e811dee08c8e886e1d394e606e018ef Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Mon, 19 Jan 2026 16:33:35 +0100 Subject: [PATCH 03/29] feat(node-console-log-transport): expose NodeConsoleLogTransport BREAKING CHANGE: - NodeConsoleLogTransport no longer stringifies but uses node:utils#inspect --- packages/logger/src/public-api.ts | 2 + .../node-console-log-transport-config.ts | 8 + .../node-console-log-transport.spec.ts | 157 ++++++++++++++++++ .../node-console-log.transport.ts | 66 ++++++++ 4 files changed, 233 insertions(+) create mode 100644 packages/logger/src/transports/node-console-log-transport/node-console-log-transport-config.ts create mode 100644 packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts create mode 100644 packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index 4281f08..ce116a4 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -4,6 +4,8 @@ export * from './model/logger.js' export * from './services/base-logger.service.js' export * from './transports/console-json-log-transport/console-json-log.transport.js' export * from './transports/console-json-log-transport/console-json-log-transport-config.js' +export * from './transports/node-console-log-transport/node-console-log.transport.js' +export * from './transports/node-console-log-transport/node-console-log-transport-config.js' export * from './utils/create-json-log-object-data.function.js' export * from './utils/format-error.function.js' export * from './utils/format-error.function.js' diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log-transport-config.ts b/packages/logger/src/transports/node-console-log-transport/node-console-log-transport-config.ts new file mode 100644 index 0000000..f2886ae --- /dev/null +++ b/packages/logger/src/transports/node-console-log-transport/node-console-log-transport-config.ts @@ -0,0 +1,8 @@ +import { InspectOptions } from 'node:util' + +import { LogLevel } from '../../model/log-level.enum.js' + +export interface NodeConsoleLogTransportConfig { + logLevel: LogLevel + nodeInspectOptions?: InspectOptions +} diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts b/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts new file mode 100644 index 0000000..ab7705d --- /dev/null +++ b/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts @@ -0,0 +1,157 @@ +import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' + +import { ConsoleMock, mockConsole, restoreConsole } from '../../../test/console-mock.function.js' +import { LogLevel } from '../../model/log-level.enum.js' +import { stringToColor } from '../../utils/logger-helper.js' +import { NodeConsoleLogTransport } from './node-console-log.transport.js' + +/** + * Formats a Date object to HH:mm:ss.SSS format + * @param {Date} date - The date to format + * @returns {string} Formatted time string in HH:mm:ss.SSS format (e.g., "14:23:45.123") + */ +function formatTime(date: Date): string { + const hours = String(date.getHours()).padStart(2, '0') + const minutes = String(date.getMinutes()).padStart(2, '0') + const seconds = String(date.getSeconds()).padStart(2, '0') + const milliseconds = String(date.getMilliseconds()).padStart(3, '0') + + return `${hours}:${minutes}:${seconds}.${milliseconds}` +} + +describe('uses console statement according to levels', () => { + let logger: NodeConsoleLogTransport + let logDate: Date + let logArgs: any[] + let formattedDate: string + let consoleMock: ConsoleMock + const className = 'MyClass' + const color = stringToColor(className) + + beforeEach(() => { + consoleMock = mockConsole() + logger = new NodeConsoleLogTransport({ logLevel: LogLevel.DEBUG }) + logDate = new Date() + logArgs = ['foo bar'] + formattedDate = formatTime(logDate) + }) + afterEach(restoreConsole) + + test('calls correct console level for DEBUG', () => { + logger.log(LogLevel.DEBUG, className, color, logDate, logArgs) + expect(consoleMock.debug).toHaveBeenCalled() + const loggedStr = consoleMock.debug.mock.calls[0][0] + expect(loggedStr.includes(className)).toBeTruthy() + expect(loggedStr.includes(logArgs[0])).toBeTruthy() + expect(loggedStr.includes(formattedDate)).toBeTruthy() + }) + test('calls correct console level for INFO', () => { + logger.log(LogLevel.INFO, className, color, logDate, logArgs) + expect(consoleMock.info).toHaveBeenCalled() + const loggedStr = consoleMock.info.mock.calls[0][0] + expect(loggedStr.includes(className)).toBeTruthy() + expect(loggedStr.includes(logArgs[0])).toBeTruthy() + expect(loggedStr.includes(formattedDate)).toBeTruthy() + }) + test('calls correct console level for WARN', () => { + logger.log(LogLevel.WARN, className, color, logDate, logArgs) + expect(consoleMock.warn).toHaveBeenCalled() + const loggedStr = consoleMock.warn.mock.calls[0][0] + expect(loggedStr.includes(className)).toBeTruthy() + expect(loggedStr.includes(logArgs[0])).toBeTruthy() + expect(loggedStr.includes(formattedDate)).toBeTruthy() + }) + test('calls correct console level for ERROR', () => { + logger.log(LogLevel.ERROR, className, color, logDate, logArgs) + expect(consoleMock.error).toHaveBeenCalled() + const loggedStr = consoleMock.error.mock.calls[0][0] + expect(loggedStr.includes(className)).toBeTruthy() + expect(loggedStr.includes(logArgs[0])).toBeTruthy() + expect(loggedStr.includes(formattedDate)).toBeTruthy() + }) +}) + +describe('respects the configured level', () => { + let consoleMock: ConsoleMock + + beforeEach(() => { + consoleMock = mockConsole() + }) + afterEach(restoreConsole) + + test('respects level DEBUG', () => { + const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.DEBUG }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(1) + expect(consoleMock.info).toHaveBeenCalledTimes(1) + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) + test('respects level INFO', () => { + const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.INFO }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(0) + expect(consoleMock.info).toHaveBeenCalledTimes(1) + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) + test('respects level WARN', () => { + const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.WARN }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(0) + expect(consoleMock.info).toHaveBeenCalledTimes(0) + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) + test('respects level ERROR', () => { + const logger = new NodeConsoleLogTransport({ logLevel: LogLevel.ERROR }) + logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) + expect(consoleMock.debug).toHaveBeenCalledTimes(0) + expect(consoleMock.info).toHaveBeenCalledTimes(0) + expect(consoleMock.warn).toHaveBeenCalledTimes(0) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) +}) + +describe('prints all the given arguments', () => { + const className = 'MyClass' + const logDate = new Date() + const color = stringToColor(className) + let logger: NodeConsoleLogTransport + let consoleMock: ConsoleMock + + beforeEach(() => { + consoleMock = mockConsole() + logger = new NodeConsoleLogTransport({ logLevel: LogLevel.DEBUG }) + }) + afterEach(restoreConsole) + + test('logs Error as Error', () => { + const error = new Error('fail') + logger.log(LogLevel.ERROR, className, color, logDate, [error]) + const usedArgs = consoleMock.error.mock.calls[0] + expect(typeof usedArgs[0]).toBe('string') + expect(usedArgs[1]).toMatch(/^Error: fail\n(\s+at.*)\n/) // stack trace follows + }) + + test('stringifies objects', () => { + const obj = { propA: true } + logger.log(LogLevel.DEBUG, className, color, logDate, [obj]) + const usedArgs = consoleMock.debug.mock.calls[0] + expect(typeof usedArgs[0]).toBe('string') + expect(usedArgs[1]).toBe('{ propA: true }') // not json, but util.inspect style + }) + +}) diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts b/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts new file mode 100644 index 0000000..a7f61a9 --- /dev/null +++ b/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts @@ -0,0 +1,66 @@ +import util from 'node:util' + +import { colorizeForConsole } from '@shiftcode/utilities' + +import { LogLevel } from '../../model/log-level.enum.js' +import { LogTransport } from '../../model/log-transport.js' +import { NodeConsoleLogTransportConfig } from './node-console-log-transport-config.js' + +export const logLevelEmoji = ['🐞', '💬', '💣', '🔥'] + +const timeFormat = new Intl.DateTimeFormat('en-US', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, + hour12: false, +}) + +export class NodeConsoleLogTransport extends LogTransport { + private readonly inspectOpts: util.InspectOptions + + constructor(config: NodeConsoleLogTransportConfig) { + super(config.logLevel) + this.inspectOpts = config.nodeInspectOptions ?? {} + } + + log(level: LogLevel, clazzName: string, hexColor: string, timestamp: Date, args: any[]) { + if (this.isLevelEnabled(level)) { + const now = timeFormat.format(timestamp) + // make sure to not alter the input args array + if (typeof args[0] === 'string') { + // if first arg is string, also colorize it + args = [ + `${logLevelEmoji[level]} ${colorizeForConsole(`${now} - ${clazzName} :: ${args[0]}`, hexColor)}`, + ...args.slice(1).map((a) => util.inspect(a, this.inspectOpts)), + ] + } else { + args = [ + `${logLevelEmoji[level]} ${colorizeForConsole(`${now} - ${clazzName} ::`, hexColor)}`, + ...args.map((a) => util.inspect(a, this.inspectOpts)) + ] + } + + /* eslint-disable prefer-spread,no-console */ + switch (level) { + case LogLevel.DEBUG: + console.debug.apply(console, args) + break + case LogLevel.ERROR: + console.error.apply(console, args) + break + case LogLevel.INFO: + console.info.apply(console, args) + break + case LogLevel.WARN: + console.warn.apply(console, args) + break + case LogLevel.OFF: + break + default: + return level // exhaustive check + } + /* eslint-enable prefer-spread,no-console */ + } + } +} From 0739de1dd9eeb20759a1c77ab0f3f44dd24b5451 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Mon, 19 Jan 2026 16:43:37 +0100 Subject: [PATCH 04/29] style(logger): format all --- packages/logger/src/testing/spy-log.transport.ts | 1 - .../console-json-log.transport.spec.ts | 2 +- .../node-console-log-transport.spec.ts | 1 - .../node-console-log.transport.ts | 2 +- .../utils/create-json-log-object-data.function.ts | 12 ++++++++---- .../get-json-stringify-replacer.function.spec.ts | 4 +--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/logger/src/testing/spy-log.transport.ts b/packages/logger/src/testing/spy-log.transport.ts index f85f680..21a1a98 100644 --- a/packages/logger/src/testing/spy-log.transport.ts +++ b/packages/logger/src/testing/spy-log.transport.ts @@ -4,7 +4,6 @@ import { jest } from '@jest/globals' import { LogLevel } from '../model/log-level.enum.js' import { LogTransport } from '../model/log-transport.js' - export class SpyLogTransport extends LogTransport { private logMock = jest.fn() diff --git a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts b/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts index f9d2594..731bb44 100644 --- a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts +++ b/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts @@ -1,4 +1,4 @@ -import { afterEach,beforeEach, describe, expect, test } from '@jest/globals' +import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' import { ConsoleMock, mockConsole, restoreConsole } from '../../../test/console-mock.function.js' import { LogLevel } from '../../model/log-level.enum.js' diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts b/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts index ab7705d..737470f 100644 --- a/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts +++ b/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts @@ -153,5 +153,4 @@ describe('prints all the given arguments', () => { expect(typeof usedArgs[0]).toBe('string') expect(usedArgs[1]).toBe('{ propA: true }') // not json, but util.inspect style }) - }) diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts b/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts index a7f61a9..5a8c1b9 100644 --- a/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts +++ b/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts @@ -37,7 +37,7 @@ export class NodeConsoleLogTransport extends LogTransport { } else { args = [ `${logLevelEmoji[level]} ${colorizeForConsole(`${now} - ${clazzName} ::`, hexColor)}`, - ...args.map((a) => util.inspect(a, this.inspectOpts)) + ...args.map((a) => util.inspect(a, this.inspectOpts)), ] } diff --git a/packages/logger/src/utils/create-json-log-object-data.function.ts b/packages/logger/src/utils/create-json-log-object-data.function.ts index ed93229..26979be 100644 --- a/packages/logger/src/utils/create-json-log-object-data.function.ts +++ b/packages/logger/src/utils/create-json-log-object-data.function.ts @@ -5,14 +5,13 @@ import { ErrorAttributes, formatError } from './format-error.function.js' // used to enforce the usage of the factory function without any runtime overhead // therefore not intended to be used in runtime code -declare const JSON_LOG_OBJECT_DATA_BRAND: unique symbol; - +declare const JSON_LOG_OBJECT_DATA_BRAND: unique symbol /** * make sure to use the {@link createJsonLogObjectData} util function to create an instance of this interface */ export interface JsonLogObjectData { - [JSON_LOG_OBJECT_DATA_BRAND]: true; + [JSON_LOG_OBJECT_DATA_BRAND]: true level: string logger: string @@ -24,7 +23,12 @@ export interface JsonLogObjectData { data?: unknown } -export function createJsonLogObjectData(level: LogLevel, clazzName: string, timestamp: Date, args: unknown[]): JsonLogObjectData { +export function createJsonLogObjectData( + level: LogLevel, + clazzName: string, + timestamp: Date, + args: unknown[], +): JsonLogObjectData { const logObjectData: Partial = { level: getEnumKeyFromNum(LogLevel, level), timestamp: timestamp.toISOString(), diff --git a/packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts b/packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts index dc07676..97c8394 100644 --- a/packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts +++ b/packages/logger/src/utils/get-json-stringify-replacer.function.spec.ts @@ -92,7 +92,7 @@ describe('getJsonStringifyReplacer', () => { it('should use custom replacer prior handling Error instances', () => { const wrapErrors = (key: string, value: unknown) => { if (value instanceof Error) { - return new Error('Failed', {cause: value}) + return new Error('Failed', { cause: value }) } return value } @@ -124,6 +124,4 @@ describe('getJsonStringifyReplacer', () => { expect(result).toContain('"bigNumber":"9007199254740991"') expect(result).toContain('"circular":""') }) - }) - From 7cf0790ae74cbc3bb79f57e47191f80b5d60c2bc Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 19 Jan 2026 15:45:43 +0000 Subject: [PATCH 05/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.0 --- package-lock.json | 6 +++--- packages/logger/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6f9fdc..c565147 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,16 +16753,16 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "3.0.4", + "version": "4.0.0-pr250.0", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" }, "engines": { - "node": "^20.0.0 || ^22.0.0" + "node": "^22.0.0 || ^24.0.0" }, "peerDependencies": { - "@shiftcode/utilities": "^4.0.0 || ^4.0.0-pr53" + "@shiftcode/utilities": "^4.0.0" } }, "packages/publish-helper": { diff --git a/packages/logger/package.json b/packages/logger/package.json index 76f98c9..5e730b4 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "3.0.4", + "version": "4.0.0-pr250.0", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From d0e6f2b09b862050b6836cc35b3ee6a113b91eb5 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Mon, 19 Jan 2026 16:54:42 +0100 Subject: [PATCH 06/29] feat(create-console-logger): utility to simply create a console logger instance basically a replacement for `simpleLogger` without dependency to `@shiftcode/lambda-utils` --- packages/logger/src/public-api.ts | 1 + .../utils/create-console-logger.function.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 packages/logger/src/utils/create-console-logger.function.ts diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index ce116a4..3dd0160 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -6,6 +6,7 @@ export * from './transports/console-json-log-transport/console-json-log.transpor export * from './transports/console-json-log-transport/console-json-log-transport-config.js' export * from './transports/node-console-log-transport/node-console-log.transport.js' export * from './transports/node-console-log-transport/node-console-log-transport-config.js' +export * from './utils/create-console-logger.function.js' export * from './utils/create-json-log-object-data.function.js' export * from './utils/format-error.function.js' export * from './utils/format-error.function.js' diff --git a/packages/logger/src/utils/create-console-logger.function.ts b/packages/logger/src/utils/create-console-logger.function.ts new file mode 100644 index 0000000..2df5bf1 --- /dev/null +++ b/packages/logger/src/utils/create-console-logger.function.ts @@ -0,0 +1,28 @@ +import { LogLevel } from '../model/log-level.enum.js' +import { LogTransport } from '../model/log-transport.js' +import { Logger } from '../model/logger.js' +import { BaseLoggerService } from '../services/base-logger.service.js' +import { ConsoleJsonLogTransport } from '../transports/console-json-log-transport/console-json-log.transport.js' +import { NodeConsoleLogTransport } from '../transports/node-console-log-transport/node-console-log.transport.js' + +/** + * Creates a simple {@link Logger} instance that logs to the console with the specified name and log level + + * @param name The name of the logger + * @param logLevel decides the minimum log level to be logged + * @param type decides whether to use {@link NodeConsoleLogTransport} or {@link ConsoleJsonLogTransport} + */ +export function createConsoleLogger(name: string, logLevel: LogLevel, type: 'node' | 'json'): Logger { + let transport: LogTransport + switch (type) { + case 'node': + transport = new NodeConsoleLogTransport({ logLevel }) + break + case 'json': + transport = new ConsoleJsonLogTransport({ logLevel }) + break + default: + transport = type + } + return new BaseLoggerService([transport]).getInstance(name) +} From 0c897a083d7a6366ee0b4cf8a8ec3d70dba41950 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 19 Jan 2026 15:57:23 +0000 Subject: [PATCH 07/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.1 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c565147..1c6359d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.0", + "version": "4.0.0-pr250.1", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index 5e730b4..3b59f80 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.0", + "version": "4.0.0-pr250.1", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From f1a97c61deb9bc64c5578e001d5e47a1a0d55bd3 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Mon, 19 Jan 2026 17:02:40 +0100 Subject: [PATCH 08/29] test(create-console-logger): test it --- .../create-console-logger.function.spec.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 packages/logger/src/utils/create-console-logger.function.spec.ts diff --git a/packages/logger/src/utils/create-console-logger.function.spec.ts b/packages/logger/src/utils/create-console-logger.function.spec.ts new file mode 100644 index 0000000..9dfefaf --- /dev/null +++ b/packages/logger/src/utils/create-console-logger.function.spec.ts @@ -0,0 +1,25 @@ +import { LogLevel } from '../model/log-level.enum.js' +import { Logger } from '../model/logger.js' +import { ConsoleJsonLogTransport } from '../transports/console-json-log-transport/console-json-log.transport.js' +import { NodeConsoleLogTransport } from '../transports/node-console-log-transport/node-console-log.transport.js' +import { createConsoleLogger } from './create-console-logger.function.js' + +describe('createConsoleLogger', () => { + it('should create logger with NodeConsoleLogTransport when type is "node"', () => { + const logger: Logger = createConsoleLogger('test-logger', LogLevel.INFO, 'node') + + expect(logger).toBeDefined() + expect(logger['loggerTransports']).toHaveLength(1) + expect(logger['loggerTransports'][0]).toBeInstanceOf(NodeConsoleLogTransport) + expect(logger['loggerTransports'][0]['logLevel']).toBe(LogLevel.INFO) + }) + + it('should create logger with ConsoleJsonLogTransport when type is "json"', () => { + const logger: Logger = createConsoleLogger('test-logger', LogLevel.DEBUG, 'json') + + expect(logger).toBeDefined() + expect(logger['loggerTransports']).toHaveLength(1) + expect(logger['loggerTransports'][0]).toBeInstanceOf(ConsoleJsonLogTransport) + expect(logger['loggerTransports'][0]['logLevel']).toBe(LogLevel.DEBUG) + }) +}) From fd5bd12219b4486166a575ea822a5b19ee33ea18 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 19 Jan 2026 16:05:11 +0000 Subject: [PATCH 09/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.2 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c6359d..640d2af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.1", + "version": "4.0.0-pr250.2", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index 3b59f80..ef9d702 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.1", + "version": "4.0.0-pr250.2", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From 9a2d0250ef066828fd9814006e341eafd9ef2673 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Tue, 20 Jan 2026 11:51:43 +0100 Subject: [PATCH 10/29] feat(create-console-logger): default to ConsoleJsonLogTransport when in awsLambda --- .../logger/src/utils/create-console-logger.function.ts | 7 +++++-- packages/logger/src/utils/is-aws-lambda-env.function.ts | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 packages/logger/src/utils/is-aws-lambda-env.function.ts diff --git a/packages/logger/src/utils/create-console-logger.function.ts b/packages/logger/src/utils/create-console-logger.function.ts index 2df5bf1..64bc4fd 100644 --- a/packages/logger/src/utils/create-console-logger.function.ts +++ b/packages/logger/src/utils/create-console-logger.function.ts @@ -4,15 +4,18 @@ import { Logger } from '../model/logger.js' import { BaseLoggerService } from '../services/base-logger.service.js' import { ConsoleJsonLogTransport } from '../transports/console-json-log-transport/console-json-log.transport.js' import { NodeConsoleLogTransport } from '../transports/node-console-log-transport/node-console-log.transport.js' +import { isAwsLambdaEnv } from './is-aws-lambda-env.function.js' /** * Creates a simple {@link Logger} instance that logs to the console with the specified name and log level * @param name The name of the logger * @param logLevel decides the minimum log level to be logged - * @param type decides whether to use {@link NodeConsoleLogTransport} or {@link ConsoleJsonLogTransport} + * @param type decides whether to use {@link NodeConsoleLogTransport} or {@link ConsoleJsonLogTransport}. + * if not set, it will default to `json` when running in AWS Lambda environment, otherwise to `node` */ -export function createConsoleLogger(name: string, logLevel: LogLevel, type: 'node' | 'json'): Logger { +export function createConsoleLogger(name: string, logLevel: LogLevel = LogLevel.DEBUG, type?: 'node' | 'json'): Logger { + type ||= isAwsLambdaEnv() ? 'json' : 'node' let transport: LogTransport switch (type) { case 'node': diff --git a/packages/logger/src/utils/is-aws-lambda-env.function.ts b/packages/logger/src/utils/is-aws-lambda-env.function.ts new file mode 100644 index 0000000..a88dfbe --- /dev/null +++ b/packages/logger/src/utils/is-aws-lambda-env.function.ts @@ -0,0 +1,5 @@ + +export function isAwsLambdaEnv(): boolean { + // aws sets the AWS_EXECUTION_ENV + return typeof globalThis.process !== 'undefined' && !!globalThis.process.env?.['AWS_EXECUTION_ENV'] +} From a29a5c49cb6ab806b46005617f13948764f8c495 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Tue, 20 Jan 2026 11:54:49 +0100 Subject: [PATCH 11/29] Revert "feat(create-console-logger): default to ConsoleJsonLogTransport when in awsLambda" This reverts commit 9a2d0250ef066828fd9814006e341eafd9ef2673. --- .../logger/src/utils/create-console-logger.function.ts | 7 ++----- packages/logger/src/utils/is-aws-lambda-env.function.ts | 5 ----- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 packages/logger/src/utils/is-aws-lambda-env.function.ts diff --git a/packages/logger/src/utils/create-console-logger.function.ts b/packages/logger/src/utils/create-console-logger.function.ts index 64bc4fd..2df5bf1 100644 --- a/packages/logger/src/utils/create-console-logger.function.ts +++ b/packages/logger/src/utils/create-console-logger.function.ts @@ -4,18 +4,15 @@ import { Logger } from '../model/logger.js' import { BaseLoggerService } from '../services/base-logger.service.js' import { ConsoleJsonLogTransport } from '../transports/console-json-log-transport/console-json-log.transport.js' import { NodeConsoleLogTransport } from '../transports/node-console-log-transport/node-console-log.transport.js' -import { isAwsLambdaEnv } from './is-aws-lambda-env.function.js' /** * Creates a simple {@link Logger} instance that logs to the console with the specified name and log level * @param name The name of the logger * @param logLevel decides the minimum log level to be logged - * @param type decides whether to use {@link NodeConsoleLogTransport} or {@link ConsoleJsonLogTransport}. - * if not set, it will default to `json` when running in AWS Lambda environment, otherwise to `node` + * @param type decides whether to use {@link NodeConsoleLogTransport} or {@link ConsoleJsonLogTransport} */ -export function createConsoleLogger(name: string, logLevel: LogLevel = LogLevel.DEBUG, type?: 'node' | 'json'): Logger { - type ||= isAwsLambdaEnv() ? 'json' : 'node' +export function createConsoleLogger(name: string, logLevel: LogLevel, type: 'node' | 'json'): Logger { let transport: LogTransport switch (type) { case 'node': diff --git a/packages/logger/src/utils/is-aws-lambda-env.function.ts b/packages/logger/src/utils/is-aws-lambda-env.function.ts deleted file mode 100644 index a88dfbe..0000000 --- a/packages/logger/src/utils/is-aws-lambda-env.function.ts +++ /dev/null @@ -1,5 +0,0 @@ - -export function isAwsLambdaEnv(): boolean { - // aws sets the AWS_EXECUTION_ENV - return typeof globalThis.process !== 'undefined' && !!globalThis.process.env?.['AWS_EXECUTION_ENV'] -} From d943d38849568d6539717b1fa2f879f35bb68a85 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Tue, 20 Jan 2026 12:00:47 +0100 Subject: [PATCH 12/29] feat(create-console-logger): add simpleLambdaLogger actually a replacement for `simpleLogger` --- packages/logger/src/public-api.ts | 1 + .../src/utils/simple-lambda-logger.function.ts | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 packages/logger/src/utils/simple-lambda-logger.function.ts diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index 3dd0160..1e17456 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -12,3 +12,4 @@ export * from './utils/format-error.function.js' export * from './utils/format-error.function.js' export * from './utils/json-log-transport.js' export * from './utils/logger-helper.js' +export * from './utils/simple-lambda-logger.function.js' diff --git a/packages/logger/src/utils/simple-lambda-logger.function.ts b/packages/logger/src/utils/simple-lambda-logger.function.ts new file mode 100644 index 0000000..2879b53 --- /dev/null +++ b/packages/logger/src/utils/simple-lambda-logger.function.ts @@ -0,0 +1,14 @@ +import { LogLevel } from '../model/log-level.enum.js' +import { createConsoleLogger } from './create-console-logger.function.js' + +function isAwsLambdaEnv(): boolean { + const env = globalThis.process?.env + // aws sets the AWS_EXECUTION_ENV so does the serverless framework - + // to detect local invocations SLS additionally sets IS_LOCAL env var + return !!(env?.['AWS_EXECUTION_ENV'] && !env?.['IS_LOCAL']) +} + +export function simpleLambdaLogger(name: string, logLevel: LogLevel = LogLevel.DEBUG) { + const isLambda = isAwsLambdaEnv() + return createConsoleLogger(name, logLevel, isLambda ? 'json' : 'node') +} From 431b606d388cdcbaf375f1a0f44c06243fc757d0 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Tue, 20 Jan 2026 11:12:21 +0000 Subject: [PATCH 13/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.3 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 640d2af..d882ac2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.2", + "version": "4.0.0-pr250.3", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index ef9d702..992e4d2 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.2", + "version": "4.0.0-pr250.3", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From 7edf992af236ab7ea862a561c4f3e8b85eb32aa6 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Tue, 20 Jan 2026 16:40:48 +0100 Subject: [PATCH 14/29] refactor(logger): deep exports for transports --- packages/logger/package.json | 8 ++++++++ packages/logger/src/public-api.ts | 4 ---- .../transports/console-json-log-transport/public-api.ts | 2 ++ .../transports/node-console-log-transport/public-api.ts | 2 ++ 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 packages/logger/src/transports/console-json-log-transport/public-api.ts create mode 100644 packages/logger/src/transports/node-console-log-transport/public-api.ts diff --git a/packages/logger/package.json b/packages/logger/package.json index 992e4d2..79bf8c0 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -12,6 +12,14 @@ "types": "./dist/public-api.d.ts", "default": "./dist/public-api.js" }, + "./transports/console-json": { + "types": "./dist/transports/console-json-log-transport/public-api.d.ts", + "default": "./dist/transports/console-json-log-transport/public-api.js" + }, + "./transports/node-console": { + "types": "./dist/transports/node-console-log-transport/public-api.d.ts", + "default": "./dist/transports/node-console-log-transport/public-api.js" + }, "./testing": { "types": "./dist/testing/public-api.d.ts", "default": "./dist/testing/public-api.js" diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index 1e17456..5e3858b 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -2,10 +2,6 @@ export * from './model/log-level.enum.js' export * from './model/log-transport.js' export * from './model/logger.js' export * from './services/base-logger.service.js' -export * from './transports/console-json-log-transport/console-json-log.transport.js' -export * from './transports/console-json-log-transport/console-json-log-transport-config.js' -export * from './transports/node-console-log-transport/node-console-log.transport.js' -export * from './transports/node-console-log-transport/node-console-log-transport-config.js' export * from './utils/create-console-logger.function.js' export * from './utils/create-json-log-object-data.function.js' export * from './utils/format-error.function.js' diff --git a/packages/logger/src/transports/console-json-log-transport/public-api.ts b/packages/logger/src/transports/console-json-log-transport/public-api.ts new file mode 100644 index 0000000..e094151 --- /dev/null +++ b/packages/logger/src/transports/console-json-log-transport/public-api.ts @@ -0,0 +1,2 @@ +export * from './console-json-log.transport.js' +export * from './console-json-log-transport-config.js' diff --git a/packages/logger/src/transports/node-console-log-transport/public-api.ts b/packages/logger/src/transports/node-console-log-transport/public-api.ts new file mode 100644 index 0000000..a55fcd5 --- /dev/null +++ b/packages/logger/src/transports/node-console-log-transport/public-api.ts @@ -0,0 +1,2 @@ +export * from './node-console-log.transport.js' +export * from './node-console-log-transport-config.js' From 7177c66a74ee8390c653c94377f663d9ec441885 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Tue, 20 Jan 2026 15:43:10 +0000 Subject: [PATCH 15/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.4 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d882ac2..53ab17c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.3", + "version": "4.0.0-pr250.4", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index 79bf8c0..31d6b79 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.3", + "version": "4.0.0-pr250.4", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From 27e21dd2d8ca4c0efe65288f7575f0880ba69d9c Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Wed, 21 Jan 2026 14:05:45 +0100 Subject: [PATCH 16/29] refactor(logger): deep exports for node related stuff --- packages/logger/package.json | 6 +-- .../console-json-log-transport-config.ts | 2 +- .../console-json-log.transport.spec.ts | 6 +-- .../console-json-log.transport.ts | 45 +++++++++++++++++++ packages/logger/src/node-api.ts | 4 ++ .../create-console-logger.function.spec.ts | 4 +- .../create-console-logger.function.ts | 4 +- .../node-console-log-transport-config.ts | 0 .../node-console-log-transport.spec.ts | 0 .../node-console-log.transport.ts | 0 .../simple-lambda-logger.function.ts | 0 packages/logger/src/public-api.ts | 4 +- .../console-json-log.transport.ts | 41 ----------------- .../console-json-log-transport/public-api.ts | 2 - .../node-console-log-transport/public-api.ts | 2 - 15 files changed, 62 insertions(+), 58 deletions(-) rename packages/logger/src/{transports => }/console-json-log-transport/console-json-log-transport-config.ts (69%) rename packages/logger/src/{transports => }/console-json-log-transport/console-json-log.transport.spec.ts (98%) create mode 100644 packages/logger/src/console-json-log-transport/console-json-log.transport.ts create mode 100644 packages/logger/src/node-api.ts rename packages/logger/src/{utils => node}/create-console-logger.function.spec.ts (82%) rename packages/logger/src/{utils => node}/create-console-logger.function.ts (81%) rename packages/logger/src/{transports => node}/node-console-log-transport/node-console-log-transport-config.ts (100%) rename packages/logger/src/{transports => node}/node-console-log-transport/node-console-log-transport.spec.ts (100%) rename packages/logger/src/{transports => node}/node-console-log-transport/node-console-log.transport.ts (100%) rename packages/logger/src/{utils => node}/simple-lambda-logger.function.ts (100%) delete mode 100644 packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts delete mode 100644 packages/logger/src/transports/console-json-log-transport/public-api.ts delete mode 100644 packages/logger/src/transports/node-console-log-transport/public-api.ts diff --git a/packages/logger/package.json b/packages/logger/package.json index 31d6b79..ad663bb 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -12,9 +12,9 @@ "types": "./dist/public-api.d.ts", "default": "./dist/public-api.js" }, - "./transports/console-json": { - "types": "./dist/transports/console-json-log-transport/public-api.d.ts", - "default": "./dist/transports/console-json-log-transport/public-api.js" + "./node": { + "types": "./dist/node-api.d.ts", + "default": "./dist/node-api.js" }, "./transports/node-console": { "types": "./dist/transports/node-console-log-transport/public-api.d.ts", diff --git a/packages/logger/src/transports/console-json-log-transport/console-json-log-transport-config.ts b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts similarity index 69% rename from packages/logger/src/transports/console-json-log-transport/console-json-log-transport-config.ts rename to packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts index be25f25..cd83a8f 100644 --- a/packages/logger/src/transports/console-json-log-transport/console-json-log-transport-config.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts @@ -1,4 +1,4 @@ -import { LogLevel } from '../../model/log-level.enum.js' +import { LogLevel } from '../model/log-level.enum.js' export interface ConsoleJsonLogTransportConfig { logLevel: LogLevel diff --git a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts similarity index 98% rename from packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts rename to packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts index 731bb44..dafda36 100644 --- a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.spec.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' -import { ConsoleMock, mockConsole, restoreConsole } from '../../../test/console-mock.function.js' -import { LogLevel } from '../../model/log-level.enum.js' -import { stringToColor } from '../../utils/logger-helper.js' +import { ConsoleMock, mockConsole, restoreConsole } from '../../test/console-mock.function.js' +import { LogLevel } from '../model/log-level.enum.js' +import { stringToColor } from '../utils/logger-helper.js' import { ConsoleJsonLogTransport } from './console-json-log.transport.js' describe('uses console statement according to levels', () => { diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts new file mode 100644 index 0000000..5ea403c --- /dev/null +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts @@ -0,0 +1,45 @@ +import { jsonMapSetStringifyReplacer } from '@shiftcode/utilities' + +import { LogLevel } from '../model/log-level.enum.js' +import { LogTransport } from '../model/log-transport.js' +import { createJsonLogObjectData } from '../utils/create-json-log-object-data.function.js' +import { getJsonStringifyReplacer } from '../utils/get-json-stringify-replacer.function.js' +import { ConsoleJsonLogTransportConfig } from './console-json-log-transport-config.js' + +export class ConsoleJsonLogTransport extends LogTransport { + private readonly jsonStringifyReplacer: (key: string, value: any) => any + + constructor(config: ConsoleJsonLogTransportConfig) { + super(config.logLevel) + this.jsonStringifyReplacer = config.jsonStringifyReplacer ?? jsonMapSetStringifyReplacer + } + + log(level: LogLevel, clazzName: string, _hexColor: string, timestamp: Date, args: any[]) { + if (this.isLevelEnabled(level)) { + const logObject = createJsonLogObjectData(level, clazzName, timestamp, args) + + const toLog = [JSON.stringify(logObject, getJsonStringifyReplacer(this.jsonStringifyReplacer))] + + /* eslint-disable prefer-spread,no-console */ + switch (level) { + case LogLevel.DEBUG: + console.debug.apply(console, toLog) + break + case LogLevel.ERROR: + console.error.apply(console, toLog) + break + case LogLevel.INFO: + console.info.apply(console, toLog) + break + case LogLevel.WARN: + console.warn.apply(console, toLog) + break + case LogLevel.OFF: + break + default: + return level // exhaustive check + } + /* eslint-enable prefer-spread,no-console */ + } + } +} diff --git a/packages/logger/src/node-api.ts b/packages/logger/src/node-api.ts new file mode 100644 index 0000000..69ae9b8 --- /dev/null +++ b/packages/logger/src/node-api.ts @@ -0,0 +1,4 @@ +export * from './node/create-console-logger.function.js' +export * from './node/node-console-log-transport/node-console-log.transport.js' +export * from './node/node-console-log-transport/node-console-log-transport-config.js' +export * from './node/simple-lambda-logger.function.js' diff --git a/packages/logger/src/utils/create-console-logger.function.spec.ts b/packages/logger/src/node/create-console-logger.function.spec.ts similarity index 82% rename from packages/logger/src/utils/create-console-logger.function.spec.ts rename to packages/logger/src/node/create-console-logger.function.spec.ts index 9dfefaf..3658063 100644 --- a/packages/logger/src/utils/create-console-logger.function.spec.ts +++ b/packages/logger/src/node/create-console-logger.function.spec.ts @@ -1,8 +1,8 @@ +import { ConsoleJsonLogTransport } from '../console-json-log-transport/console-json-log.transport.js' import { LogLevel } from '../model/log-level.enum.js' import { Logger } from '../model/logger.js' -import { ConsoleJsonLogTransport } from '../transports/console-json-log-transport/console-json-log.transport.js' -import { NodeConsoleLogTransport } from '../transports/node-console-log-transport/node-console-log.transport.js' import { createConsoleLogger } from './create-console-logger.function.js' +import { NodeConsoleLogTransport } from './node-console-log-transport/node-console-log.transport.js' describe('createConsoleLogger', () => { it('should create logger with NodeConsoleLogTransport when type is "node"', () => { diff --git a/packages/logger/src/utils/create-console-logger.function.ts b/packages/logger/src/node/create-console-logger.function.ts similarity index 81% rename from packages/logger/src/utils/create-console-logger.function.ts rename to packages/logger/src/node/create-console-logger.function.ts index 2df5bf1..52ca7f1 100644 --- a/packages/logger/src/utils/create-console-logger.function.ts +++ b/packages/logger/src/node/create-console-logger.function.ts @@ -1,9 +1,9 @@ +import { ConsoleJsonLogTransport } from '../console-json-log-transport/console-json-log.transport.js' import { LogLevel } from '../model/log-level.enum.js' import { LogTransport } from '../model/log-transport.js' import { Logger } from '../model/logger.js' import { BaseLoggerService } from '../services/base-logger.service.js' -import { ConsoleJsonLogTransport } from '../transports/console-json-log-transport/console-json-log.transport.js' -import { NodeConsoleLogTransport } from '../transports/node-console-log-transport/node-console-log.transport.js' +import { NodeConsoleLogTransport } from './node-console-log-transport/node-console-log.transport.js' /** * Creates a simple {@link Logger} instance that logs to the console with the specified name and log level diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log-transport-config.ts b/packages/logger/src/node/node-console-log-transport/node-console-log-transport-config.ts similarity index 100% rename from packages/logger/src/transports/node-console-log-transport/node-console-log-transport-config.ts rename to packages/logger/src/node/node-console-log-transport/node-console-log-transport-config.ts diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts b/packages/logger/src/node/node-console-log-transport/node-console-log-transport.spec.ts similarity index 100% rename from packages/logger/src/transports/node-console-log-transport/node-console-log-transport.spec.ts rename to packages/logger/src/node/node-console-log-transport/node-console-log-transport.spec.ts diff --git a/packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts b/packages/logger/src/node/node-console-log-transport/node-console-log.transport.ts similarity index 100% rename from packages/logger/src/transports/node-console-log-transport/node-console-log.transport.ts rename to packages/logger/src/node/node-console-log-transport/node-console-log.transport.ts diff --git a/packages/logger/src/utils/simple-lambda-logger.function.ts b/packages/logger/src/node/simple-lambda-logger.function.ts similarity index 100% rename from packages/logger/src/utils/simple-lambda-logger.function.ts rename to packages/logger/src/node/simple-lambda-logger.function.ts diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index 5e3858b..0f517d9 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -1,11 +1,11 @@ +export * from './console-json-log-transport/console-json-log.transport.js' +export * from './console-json-log-transport/console-json-log-transport-config.js' export * from './model/log-level.enum.js' export * from './model/log-transport.js' export * from './model/logger.js' export * from './services/base-logger.service.js' -export * from './utils/create-console-logger.function.js' export * from './utils/create-json-log-object-data.function.js' export * from './utils/format-error.function.js' export * from './utils/format-error.function.js' export * from './utils/json-log-transport.js' export * from './utils/logger-helper.js' -export * from './utils/simple-lambda-logger.function.js' diff --git a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts deleted file mode 100644 index 81a553d..0000000 --- a/packages/logger/src/transports/console-json-log-transport/console-json-log.transport.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { jsonMapSetStringifyReplacer } from '@shiftcode/utilities' - -import { LogLevel } from '../../model/log-level.enum.js' -import { JsonLogObjectData } from '../../utils/create-json-log-object-data.function.js' -import { getJsonStringifyReplacer } from '../../utils/get-json-stringify-replacer.function.js' -import { JsonLogTransport } from '../../utils/json-log-transport.js' -import { ConsoleJsonLogTransportConfig } from './console-json-log-transport-config.js' - -export class ConsoleJsonLogTransport extends JsonLogTransport { - private readonly jsonStringifyReplacer: (key: string, value: any) => any - - constructor(config: ConsoleJsonLogTransportConfig) { - super(config.logLevel) - this.jsonStringifyReplacer = config.jsonStringifyReplacer ?? jsonMapSetStringifyReplacer - } - - transportLog(level: LogLevel, logDataObject: JsonLogObjectData): void { - const toLog = [JSON.stringify(logDataObject, getJsonStringifyReplacer(this.jsonStringifyReplacer))] - - /* eslint-disable prefer-spread,no-console */ - switch (level) { - case LogLevel.DEBUG: - console.debug.apply(console, toLog) - break - case LogLevel.ERROR: - console.error.apply(console, toLog) - break - case LogLevel.INFO: - console.info.apply(console, toLog) - break - case LogLevel.WARN: - console.warn.apply(console, toLog) - break - case LogLevel.OFF: - break - default: - return level // exhaustive check - } - /* eslint-enable prefer-spread,no-console */ - } -} diff --git a/packages/logger/src/transports/console-json-log-transport/public-api.ts b/packages/logger/src/transports/console-json-log-transport/public-api.ts deleted file mode 100644 index e094151..0000000 --- a/packages/logger/src/transports/console-json-log-transport/public-api.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './console-json-log.transport.js' -export * from './console-json-log-transport-config.js' diff --git a/packages/logger/src/transports/node-console-log-transport/public-api.ts b/packages/logger/src/transports/node-console-log-transport/public-api.ts deleted file mode 100644 index a55fcd5..0000000 --- a/packages/logger/src/transports/node-console-log-transport/public-api.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './node-console-log.transport.js' -export * from './node-console-log-transport-config.js' From 2bf7660beee84a577bac25141e69b21d7316ef64 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 21 Jan 2026 13:08:07 +0000 Subject: [PATCH 17/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.5 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53ab17c..a3304bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.4", + "version": "4.0.0-pr250.5", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index ad663bb..04cde9f 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.4", + "version": "4.0.0-pr250.5", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From d2cde151877fa2a064b3ef25713878d80360c41c Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Wed, 21 Jan 2026 15:53:09 +0100 Subject: [PATCH 18/29] feat(logger): expose getJsonStringifyReplacer utility --- packages/logger/src/public-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index 0f517d9..f375ec3 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -6,6 +6,6 @@ export * from './model/logger.js' export * from './services/base-logger.service.js' export * from './utils/create-json-log-object-data.function.js' export * from './utils/format-error.function.js' -export * from './utils/format-error.function.js' +export * from './utils/get-json-stringify-replacer.function.js' export * from './utils/json-log-transport.js' export * from './utils/logger-helper.js' From 0e312d5bd821eb278da07f772964d6b2775a2bb6 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 21 Jan 2026 14:55:56 +0000 Subject: [PATCH 19/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.6 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3304bb..4d84695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.5", + "version": "4.0.0-pr250.6", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index 04cde9f..2365e89 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.5", + "version": "4.0.0-pr250.6", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From 0db825f1d586d95e9fb9a5577d6fdfa8409cfa23 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 23 Jan 2026 10:42:48 +0100 Subject: [PATCH 20/29] feat(logger): below-level buffering for console-json logger --- packages/logger/package.json | 2 +- .../console-json-log-transport-config.ts | 13 ++ .../console-json-log.transport.spec.ts | 119 ++++++++++++++++-- .../console-json-log.transport.ts | 84 +++++++++---- packages/logger/src/public-api.ts | 1 + .../push-to-ring-buffer.function.spec.ts | 38 ++++++ .../src/utils/push-to-ring-buffer.function.ts | 16 +++ 7 files changed, 238 insertions(+), 35 deletions(-) create mode 100644 packages/logger/src/utils/push-to-ring-buffer.function.spec.ts create mode 100644 packages/logger/src/utils/push-to-ring-buffer.function.ts diff --git a/packages/logger/package.json b/packages/logger/package.json index 2365e89..0228e64 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -36,7 +36,7 @@ "lint:ci": "eslint ./src ./test", "lint:staged": "eslint --fix --cache", "prepublish": "node ../publish-helper/dist/prepare-dist.js", - "test": "NODE_OPTIONS=\"--experimental-vm-modules --trace-warnings\" npx jest --config jest.config.js", + "test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings\" npx jest --config jest.config.js", "test:ci": "npm run test", "test:watch": "npm run test -- --watch" }, diff --git a/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts index cd83a8f..e14bb3b 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts @@ -3,4 +3,17 @@ import { LogLevel } from '../model/log-level.enum.js' export interface ConsoleJsonLogTransportConfig { logLevel: LogLevel jsonStringifyReplacer?: (key: string, value: any) => any + + /** + * Log messages below the configured level will be buffered up to this size + * and flushed when a log event with level Error occurs. + * set to 0 to disable this "below-level" buffering. + * @default 50 + */ + belowLevelLogBufferSize?: number + + /** + * when true, the log output will be a JS object instead of a JSON string. {@jsonStringifyReplacer} is still used. + */ + logJsObject?: boolean } diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts index dafda36..6b44b49 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts @@ -78,7 +78,7 @@ describe('respects the configured level', () => { afterEach(restoreConsole) test('respects level DEBUG', () => { - const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.DEBUG }) + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.DEBUG, belowLevelLogBufferSize: 0 }) logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) @@ -89,8 +89,8 @@ describe('respects the configured level', () => { expect(consoleMock.error).toHaveBeenCalledTimes(1) }) test('respects level INFO', () => { - const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.INFO }) - logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.INFO, belowLevelLogBufferSize: 0 }) + // do not log DEBUG to keep this focused on allowed levels only logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) @@ -100,9 +100,8 @@ describe('respects the configured level', () => { expect(consoleMock.error).toHaveBeenCalledTimes(1) }) test('respects level WARN', () => { - const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.WARN }) - logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) - logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.WARN, belowLevelLogBufferSize: 0 }) + // do not log DEBUG/INFO to keep this focused on allowed levels only logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) expect(consoleMock.debug).toHaveBeenCalledTimes(0) @@ -111,10 +110,8 @@ describe('respects the configured level', () => { expect(consoleMock.error).toHaveBeenCalledTimes(1) }) test('respects level ERROR', () => { - const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.ERROR }) - logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug']) - logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info']) - logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn']) + const logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.ERROR, belowLevelLogBufferSize: 0 }) + // only log ERROR as allowed logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error']) expect(consoleMock.debug).toHaveBeenCalledTimes(0) expect(consoleMock.info).toHaveBeenCalledTimes(0) @@ -244,3 +241,105 @@ describe('handles circular references in log data', () => { ) }) }) + +// New tests for buffering behavior driven by belowLevelLogBufferSize + +describe('below-level buffering and flush on ERROR', () => { + let logger: ConsoleJsonLogTransport + let consoleMock: ConsoleMock + + beforeEach(() => void (consoleMock = mockConsole())) + afterEach(restoreConsole) + + const parseCallArg = (arg: unknown) => JSON.parse(arg as string) as { level: string; message: string } + + test('buffers below-level DEBUG/WARN logs and flushes them before ERROR', () => { + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.ERROR, belowLevelLogBufferSize: 10 }) + + const ts = new Date() + const color = stringToColor('MyClass') + + logger.log(LogLevel.DEBUG, 'MyClass', color, ts, ['debug-1']) + logger.log(LogLevel.WARN, 'MyClass', color, ts, ['warn-1']) + + // trigger flush + logger.log(LogLevel.ERROR, 'MyClass', color, ts, ['error-1']) + + expect(consoleMock.debug).toHaveBeenCalledTimes(1) + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(1) + + // verify order: flushed entries were logged before the ERROR + const debugOrder = consoleMock.debug.mock.invocationCallOrder[0] + const warnOrder = consoleMock.warn.mock.invocationCallOrder[0] + const errorOrder = consoleMock.error.mock.invocationCallOrder[0] + expect(debugOrder).toBeLessThan(warnOrder) + expect(warnOrder).toBeLessThan(errorOrder) + + // verify messages preserved + const debugPayload = parseCallArg(consoleMock.debug.mock.calls[0][0]) + const warnPayload = parseCallArg(consoleMock.warn.mock.calls[0][0]) + const errorPayload = parseCallArg(consoleMock.error.mock.calls[0][0]) + + expect(debugPayload).toEqual(expect.objectContaining({ level: 'DEBUG', message: 'debug-1' })) + expect(warnPayload).toEqual(expect.objectContaining({ level: 'WARN', message: 'warn-1' })) + expect(errorPayload).toEqual(expect.objectContaining({ level: 'ERROR', message: 'error-1' })) + }) + + test('respects buffer size: only keep the last N items', () => { + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.ERROR, belowLevelLogBufferSize: 2 }) + + const ts = new Date() + const color = stringToColor('MyClass') + + logger.log(LogLevel.DEBUG, 'MyClass', color, ts, ['a']) + logger.log(LogLevel.DEBUG, 'MyClass', color, ts, ['b']) + logger.log(LogLevel.DEBUG, 'MyClass', color, ts, ['c']) // should evict 'a' + + logger.log(LogLevel.ERROR, 'MyClass', color, ts, ['boom']) + + expect(consoleMock.debug).toHaveBeenCalledTimes(2) + const flushedMessages = consoleMock.debug.mock.calls.map((c: any[]) => parseCallArg(c[0]).message) + expect(flushedMessages).toEqual(['b', 'c']) + }) + + test('buffer size 0 disables buffering', () => { + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.ERROR, belowLevelLogBufferSize: 0 }) + + const ts = new Date() + const color = stringToColor('MyClass') + + logger.log(LogLevel.DEBUG, 'MyClass', color, ts, ['pre']) // not kept + logger.log(LogLevel.ERROR, 'MyClass', color, ts, ['err']) + + expect(consoleMock.debug).not.toHaveBeenCalled() + expect(consoleMock.error).toHaveBeenCalledTimes(1) + }) + + test('does not flush when ERROR is not enabled (log level OFF)', () => { + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.OFF, belowLevelLogBufferSize: 5 }) + + const ts = new Date() + + logger.log(LogLevel.DEBUG, 'MyClass', '#000000', ts, ['pre']) + logger.log(LogLevel.ERROR, 'MyClass', '#000000', ts, ['err']) // below-level, so no flush + + expect(consoleMock.debug).not.toHaveBeenCalled() + expect(consoleMock.info).not.toHaveBeenCalled() + expect(consoleMock.warn).not.toHaveBeenCalled() + expect(consoleMock.error).not.toHaveBeenCalled() + }) + + test('clears buffer after flushing on ERROR', () => { + logger = new ConsoleJsonLogTransport({ logLevel: LogLevel.ERROR, belowLevelLogBufferSize: 5 }) + + const ts = new Date() + + logger.log(LogLevel.DEBUG, 'MyClass', '#000000', ts, ['once']) + logger.log(LogLevel.ERROR, 'MyClass', '#000000', ts, ['first']) // flushes 'once' + logger.log(LogLevel.ERROR, 'MyClass', '#000000', ts, ['second']) // no additional debug flushed + + expect(consoleMock.debug).toHaveBeenCalledTimes(1) + expect(consoleMock.error).toHaveBeenCalledTimes(2) + }) +}) diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts index 5ea403c..9626562 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts @@ -4,42 +4,78 @@ import { LogLevel } from '../model/log-level.enum.js' import { LogTransport } from '../model/log-transport.js' import { createJsonLogObjectData } from '../utils/create-json-log-object-data.function.js' import { getJsonStringifyReplacer } from '../utils/get-json-stringify-replacer.function.js' +import { pushToRingBuffer } from '../utils/push-to-ring-buffer.function.js' import { ConsoleJsonLogTransportConfig } from './console-json-log-transport-config.js' +interface BufferedLogMessage { + level: LogLevel + message: string +} + export class ConsoleJsonLogTransport extends LogTransport { + private static readonly DEFAULT_BUFFER_SIZE = 50 + private readonly bufferSize:number + + /** Ring buffer for log events below the configured level, flushed on ERROR */ + private pendingBuffer: BufferedLogMessage[] = [] + private readonly jsonStringifyReplacer: (key: string, value: any) => any constructor(config: ConsoleJsonLogTransportConfig) { super(config.logLevel) + this.bufferSize = config.belowLevelLogBufferSize ?? ConsoleJsonLogTransport.DEFAULT_BUFFER_SIZE this.jsonStringifyReplacer = config.jsonStringifyReplacer ?? jsonMapSetStringifyReplacer } log(level: LogLevel, clazzName: string, _hexColor: string, timestamp: Date, args: any[]) { - if (this.isLevelEnabled(level)) { - const logObject = createJsonLogObjectData(level, clazzName, timestamp, args) - - const toLog = [JSON.stringify(logObject, getJsonStringifyReplacer(this.jsonStringifyReplacer))] - - /* eslint-disable prefer-spread,no-console */ - switch (level) { - case LogLevel.DEBUG: - console.debug.apply(console, toLog) - break - case LogLevel.ERROR: - console.error.apply(console, toLog) - break - case LogLevel.INFO: - console.info.apply(console, toLog) - break - case LogLevel.WARN: - console.warn.apply(console, toLog) - break - case LogLevel.OFF: - break - default: - return level // exhaustive check + if (level === LogLevel.OFF) { + return + } + + const logObject = createJsonLogObjectData(level, clazzName, timestamp, args) + + // we stringify at this point (instead of postpone it to when logging actually happens) + // to cut any potential references to objects that could potentially change until the log is actually written. + const message = JSON.stringify(logObject, getJsonStringifyReplacer(this.jsonStringifyReplacer)) + + if (!this.isLevelEnabled(level)) { + pushToRingBuffer(this.pendingBuffer, { level, message }, this.bufferSize) + return + } + + // on error enabled: flush buffered events first, then clear buffer + if (level === LogLevel.ERROR) { + for (const bufferedEntry of this.pendingBuffer) { + this.logToConsole(bufferedEntry.level, bufferedEntry.message) } - /* eslint-enable prefer-spread,no-console */ + this.pendingBuffer = [] + } + + // log current message + this.logToConsole(level, message) + } + + protected logToConsole(level: LogLevel, toLog: unknown) { + + /* eslint-disable no-console */ + switch (level) { + case LogLevel.DEBUG: + console.debug(toLog) + break + case LogLevel.ERROR: + console.error(toLog) + break + case LogLevel.INFO: + console.info(toLog) + break + case LogLevel.WARN: + console.warn(toLog) + break + case LogLevel.OFF: + break + default: + return level // exhaustive check } + /* eslint-enable no-console */ } } diff --git a/packages/logger/src/public-api.ts b/packages/logger/src/public-api.ts index f375ec3..9ca2ca2 100644 --- a/packages/logger/src/public-api.ts +++ b/packages/logger/src/public-api.ts @@ -9,3 +9,4 @@ export * from './utils/format-error.function.js' export * from './utils/get-json-stringify-replacer.function.js' export * from './utils/json-log-transport.js' export * from './utils/logger-helper.js' +export * from './utils/push-to-ring-buffer.function.js' diff --git a/packages/logger/src/utils/push-to-ring-buffer.function.spec.ts b/packages/logger/src/utils/push-to-ring-buffer.function.spec.ts new file mode 100644 index 0000000..946041d --- /dev/null +++ b/packages/logger/src/utils/push-to-ring-buffer.function.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect } from '@jest/globals' + +import { pushToRingBuffer } from './push-to-ring-buffer.function.js' + +describe('pushToRingBuffer', () => { + it('should push items to the buffer and maintain max size', () => { + const buffer: number[] = [] + const maxSize = 3 + + pushToRingBuffer(buffer, 1, maxSize) + pushToRingBuffer(buffer, 2, maxSize) + pushToRingBuffer(buffer, 3, maxSize) + expect(buffer).toEqual([1, 2, 3]) + + pushToRingBuffer(buffer, 4, maxSize) + expect(buffer).toEqual([2, 3, 4]) + }) + + it('removes all items that are too many and keeps the tail', () => { + const buffer = [1, 2, 3, 4] + const maxSize = 2 + + pushToRingBuffer(buffer, 5, maxSize) + expect(buffer).toEqual([4, 5]) + }) + + it('does not throw or fail for negative maxSize but simply clears the buffer', () => { + const buffer1: string[] = ['foo'] + pushToRingBuffer(buffer1, 'bar', -1) + expect(buffer1).toEqual([]) + }) + + it('does not throw or fail for zero maxSize but simply clears the buffer', () => { + const buffer2: string[] = ['foo'] + pushToRingBuffer(buffer2, 'bar', 0) + expect(buffer2).toEqual([]) + }) +}) diff --git a/packages/logger/src/utils/push-to-ring-buffer.function.ts b/packages/logger/src/utils/push-to-ring-buffer.function.ts new file mode 100644 index 0000000..2334930 --- /dev/null +++ b/packages/logger/src/utils/push-to-ring-buffer.function.ts @@ -0,0 +1,16 @@ +/** + * Push an item to a ring buffer array. If the buffer exceeds maxSize, the oldest entry is removed. + */ +export function pushToRingBuffer(buffer: T[], item: T, maxSize: number): void { + if (maxSize <= 0) { + while(buffer.shift()) { + // just clears the buffer + } + return + } + + buffer.push(item) + while (buffer.length > maxSize) { + buffer.shift() + } +} From ee039b9fba50d00164a3856d12161cbd11d3ecbd Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 23 Jan 2026 10:52:23 +0100 Subject: [PATCH 21/29] style(format): why necessary? --- .../console-json-log-transport/console-json-log.transport.ts | 3 +-- packages/logger/src/utils/push-to-ring-buffer.function.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts index 9626562..6d49539 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts @@ -14,7 +14,7 @@ interface BufferedLogMessage { export class ConsoleJsonLogTransport extends LogTransport { private static readonly DEFAULT_BUFFER_SIZE = 50 - private readonly bufferSize:number + private readonly bufferSize: number /** Ring buffer for log events below the configured level, flushed on ERROR */ private pendingBuffer: BufferedLogMessage[] = [] @@ -56,7 +56,6 @@ export class ConsoleJsonLogTransport extends LogTransport { } protected logToConsole(level: LogLevel, toLog: unknown) { - /* eslint-disable no-console */ switch (level) { case LogLevel.DEBUG: diff --git a/packages/logger/src/utils/push-to-ring-buffer.function.ts b/packages/logger/src/utils/push-to-ring-buffer.function.ts index 2334930..381fa7b 100644 --- a/packages/logger/src/utils/push-to-ring-buffer.function.ts +++ b/packages/logger/src/utils/push-to-ring-buffer.function.ts @@ -3,8 +3,8 @@ */ export function pushToRingBuffer(buffer: T[], item: T, maxSize: number): void { if (maxSize <= 0) { - while(buffer.shift()) { - // just clears the buffer + while (buffer.shift()) { + // just clears the buffer } return } From c07ff7a7d0ee757061ba92b0678be9c02f8eaa35 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 23 Jan 2026 09:54:44 +0000 Subject: [PATCH 22/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.7 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d84695..d558fca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.6", + "version": "4.0.0-pr250.7", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index 0228e64..3620716 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.6", + "version": "4.0.0-pr250.7", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From 551cb23d9ce2f80ce91356d72d96ebee0c708dbd Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 23 Jan 2026 15:25:38 +0100 Subject: [PATCH 23/29] refactor(logger): utility functions --- packages/logger/package.json | 8 ++------ .../console-json-log-transport-config.ts | 2 ++ .../console-json-log.transport.ts | 5 ++++- packages/logger/src/node-api.ts | 4 ---- .../src/node/create-console-logger.function.ts | 18 ++++++++++++++---- .../node-console-log.transport.ts | 2 +- packages/logger/src/node/public-api.ts | 4 ++++ .../src/node/simple-lambda-logger.function.ts | 2 +- 8 files changed, 28 insertions(+), 17 deletions(-) delete mode 100644 packages/logger/src/node-api.ts create mode 100644 packages/logger/src/node/public-api.ts diff --git a/packages/logger/package.json b/packages/logger/package.json index 3620716..8bd96c5 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -13,12 +13,8 @@ "default": "./dist/public-api.js" }, "./node": { - "types": "./dist/node-api.d.ts", - "default": "./dist/node-api.js" - }, - "./transports/node-console": { - "types": "./dist/transports/node-console-log-transport/public-api.d.ts", - "default": "./dist/transports/node-console-log-transport/public-api.js" + "types": "./dist/node/public-api.d.ts", + "default": "./dist/node/public-api.js" }, "./testing": { "types": "./dist/testing/public-api.d.ts", diff --git a/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts index e14bb3b..ca5a25c 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts @@ -14,6 +14,8 @@ export interface ConsoleJsonLogTransportConfig { /** * when true, the log output will be a JS object instead of a JSON string. {@jsonStringifyReplacer} is still used. + * enable this option, when your lambda function is configured with `loggingFormat='JSON'` + * @hint when loggingFormat='JSON' is set, you should also configure `applicationLogLevelV2` with `TRACE` - otherwise you won't see all logging output. */ logJsObject?: boolean } diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts index 6d49539..2714567 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts @@ -19,10 +19,12 @@ export class ConsoleJsonLogTransport extends LogTransport { /** Ring buffer for log events below the configured level, flushed on ERROR */ private pendingBuffer: BufferedLogMessage[] = [] + private readonly logJsObject: boolean private readonly jsonStringifyReplacer: (key: string, value: any) => any constructor(config: ConsoleJsonLogTransportConfig) { super(config.logLevel) + this.logJsObject = config.logJsObject ?? false this.bufferSize = config.belowLevelLogBufferSize ?? ConsoleJsonLogTransport.DEFAULT_BUFFER_SIZE this.jsonStringifyReplacer = config.jsonStringifyReplacer ?? jsonMapSetStringifyReplacer } @@ -38,6 +40,7 @@ export class ConsoleJsonLogTransport extends LogTransport { // to cut any potential references to objects that could potentially change until the log is actually written. const message = JSON.stringify(logObject, getJsonStringifyReplacer(this.jsonStringifyReplacer)) + if (!this.isLevelEnabled(level)) { pushToRingBuffer(this.pendingBuffer, { level, message }, this.bufferSize) return @@ -55,7 +58,7 @@ export class ConsoleJsonLogTransport extends LogTransport { this.logToConsole(level, message) } - protected logToConsole(level: LogLevel, toLog: unknown) { + protected logToConsole(level: LogLevel, toLog: string) { /* eslint-disable no-console */ switch (level) { case LogLevel.DEBUG: diff --git a/packages/logger/src/node-api.ts b/packages/logger/src/node-api.ts deleted file mode 100644 index 69ae9b8..0000000 --- a/packages/logger/src/node-api.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './node/create-console-logger.function.js' -export * from './node/node-console-log-transport/node-console-log.transport.js' -export * from './node/node-console-log-transport/node-console-log-transport-config.js' -export * from './node/simple-lambda-logger.function.js' diff --git a/packages/logger/src/node/create-console-logger.function.ts b/packages/logger/src/node/create-console-logger.function.ts index 52ca7f1..bd64cda 100644 --- a/packages/logger/src/node/create-console-logger.function.ts +++ b/packages/logger/src/node/create-console-logger.function.ts @@ -1,25 +1,35 @@ import { ConsoleJsonLogTransport } from '../console-json-log-transport/console-json-log.transport.js' -import { LogLevel } from '../model/log-level.enum.js' +import { ConsoleJsonLogTransportConfig } from '../console-json-log-transport/console-json-log-transport-config.js' import { LogTransport } from '../model/log-transport.js' import { Logger } from '../model/logger.js' import { BaseLoggerService } from '../services/base-logger.service.js' import { NodeConsoleLogTransport } from './node-console-log-transport/node-console-log.transport.js' +import { NodeConsoleLogTransportConfig } from './node-console-log-transport/node-console-log-transport-config.js' +type Config = { + node: NodeConsoleLogTransportConfig + json: ConsoleJsonLogTransportConfig +} /** * Creates a simple {@link Logger} instance that logs to the console with the specified name and log level * @param name The name of the logger * @param logLevel decides the minimum log level to be logged * @param type decides whether to use {@link NodeConsoleLogTransport} or {@link ConsoleJsonLogTransport} + * @param config optional configuration for the selected transport */ -export function createConsoleLogger(name: string, logLevel: LogLevel, type: 'node' | 'json'): Logger { +export function createConsoleLogger( + name: string, + config: Config, + type: 'node' | 'json', +): Logger { let transport: LogTransport switch (type) { case 'node': - transport = new NodeConsoleLogTransport({ logLevel }) + transport = new NodeConsoleLogTransport(config.node) break case 'json': - transport = new ConsoleJsonLogTransport({ logLevel }) + transport = new ConsoleJsonLogTransport(config.json) break default: transport = type diff --git a/packages/logger/src/node/node-console-log-transport/node-console-log.transport.ts b/packages/logger/src/node/node-console-log-transport/node-console-log.transport.ts index 5a8c1b9..579668c 100644 --- a/packages/logger/src/node/node-console-log-transport/node-console-log.transport.ts +++ b/packages/logger/src/node/node-console-log-transport/node-console-log.transport.ts @@ -6,7 +6,7 @@ import { LogLevel } from '../../model/log-level.enum.js' import { LogTransport } from '../../model/log-transport.js' import { NodeConsoleLogTransportConfig } from './node-console-log-transport-config.js' -export const logLevelEmoji = ['🐞', '💬', '💣', '🔥'] +export const logLevelEmoji: Record = ['🐞', '💬', '💣', '🔥', ''] const timeFormat = new Intl.DateTimeFormat('en-US', { hour: '2-digit', diff --git a/packages/logger/src/node/public-api.ts b/packages/logger/src/node/public-api.ts new file mode 100644 index 0000000..d0bc943 --- /dev/null +++ b/packages/logger/src/node/public-api.ts @@ -0,0 +1,4 @@ +export * from './create-console-logger.function.js' +export * from './node-console-log-transport/node-console-log.transport.js' +export * from './node-console-log-transport/node-console-log-transport-config.js' +export * from './simple-lambda-logger.function.js' diff --git a/packages/logger/src/node/simple-lambda-logger.function.ts b/packages/logger/src/node/simple-lambda-logger.function.ts index 2879b53..eddb3a7 100644 --- a/packages/logger/src/node/simple-lambda-logger.function.ts +++ b/packages/logger/src/node/simple-lambda-logger.function.ts @@ -10,5 +10,5 @@ function isAwsLambdaEnv(): boolean { export function simpleLambdaLogger(name: string, logLevel: LogLevel = LogLevel.DEBUG) { const isLambda = isAwsLambdaEnv() - return createConsoleLogger(name, logLevel, isLambda ? 'json' : 'node') + return createConsoleLogger(name, { json: { logLevel }, node: { logLevel } }, isLambda ? 'json' : 'node') } From 052f4c6da5d20c8751a5e6c7358f8ef5c7ea1641 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 23 Jan 2026 16:09:57 +0100 Subject: [PATCH 24/29] refactor(logger): utility functions --- .../node/create-console-logger.function.spec.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/logger/src/node/create-console-logger.function.spec.ts b/packages/logger/src/node/create-console-logger.function.spec.ts index 3658063..b36f61c 100644 --- a/packages/logger/src/node/create-console-logger.function.spec.ts +++ b/packages/logger/src/node/create-console-logger.function.spec.ts @@ -6,20 +6,28 @@ import { NodeConsoleLogTransport } from './node-console-log-transport/node-conso describe('createConsoleLogger', () => { it('should create logger with NodeConsoleLogTransport when type is "node"', () => { - const logger: Logger = createConsoleLogger('test-logger', LogLevel.INFO, 'node') + const logger: Logger = createConsoleLogger( + 'test-logger', + { node: { logLevel: LogLevel.DEBUG }, json: { logLevel: LogLevel.ERROR } }, + 'node', + ) expect(logger).toBeDefined() expect(logger['loggerTransports']).toHaveLength(1) expect(logger['loggerTransports'][0]).toBeInstanceOf(NodeConsoleLogTransport) - expect(logger['loggerTransports'][0]['logLevel']).toBe(LogLevel.INFO) + expect(logger['loggerTransports'][0]['logLevel']).toBe(LogLevel.DEBUG) }) it('should create logger with ConsoleJsonLogTransport when type is "json"', () => { - const logger: Logger = createConsoleLogger('test-logger', LogLevel.DEBUG, 'json') + const logger: Logger = createConsoleLogger( + 'test-logger', + { node: { logLevel: LogLevel.DEBUG }, json: { logLevel: LogLevel.ERROR } }, + 'json', + ) expect(logger).toBeDefined() expect(logger['loggerTransports']).toHaveLength(1) expect(logger['loggerTransports'][0]).toBeInstanceOf(ConsoleJsonLogTransport) - expect(logger['loggerTransports'][0]['logLevel']).toBe(LogLevel.DEBUG) + expect(logger['loggerTransports'][0]['logLevel']).toBe(LogLevel.ERROR) }) }) From 8edcbb84d40ebf05af9365af4496bba86b1ab5a3 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 23 Jan 2026 16:13:37 +0100 Subject: [PATCH 25/29] refactor(logger): utility functions --- .../console-json-log.transport.ts | 1 - packages/logger/src/node/create-console-logger.function.ts | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts index 2714567..89111a3 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts @@ -40,7 +40,6 @@ export class ConsoleJsonLogTransport extends LogTransport { // to cut any potential references to objects that could potentially change until the log is actually written. const message = JSON.stringify(logObject, getJsonStringifyReplacer(this.jsonStringifyReplacer)) - if (!this.isLevelEnabled(level)) { pushToRingBuffer(this.pendingBuffer, { level, message }, this.bufferSize) return diff --git a/packages/logger/src/node/create-console-logger.function.ts b/packages/logger/src/node/create-console-logger.function.ts index bd64cda..0666cd7 100644 --- a/packages/logger/src/node/create-console-logger.function.ts +++ b/packages/logger/src/node/create-console-logger.function.ts @@ -18,11 +18,7 @@ type Config = { * @param type decides whether to use {@link NodeConsoleLogTransport} or {@link ConsoleJsonLogTransport} * @param config optional configuration for the selected transport */ -export function createConsoleLogger( - name: string, - config: Config, - type: 'node' | 'json', -): Logger { +export function createConsoleLogger(name: string, config: Config, type: 'node' | 'json'): Logger { let transport: LogTransport switch (type) { case 'node': From 5df0017e8cc98b4c54d405e800f2aca9f7f8f519 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 23 Jan 2026 15:15:55 +0000 Subject: [PATCH 26/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.8 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d558fca..9f62a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.7", + "version": "4.0.0-pr250.8", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index 8bd96c5..b085537 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.7", + "version": "4.0.0-pr250.8", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED", From c688c6d6fe3da85053f4707de009780b3562fb53 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 23 Jan 2026 16:42:49 +0100 Subject: [PATCH 27/29] refactor(console-json-log): actually implement and test logJsObject --- .../console-json-log-transport-config.ts | 6 ++ .../console-json-log.transport.spec.ts | 55 ++++++++++++++++++- .../console-json-log.transport.ts | 3 + .../node-console-log-transport-config.ts | 5 ++ .../node-console-log-transport.spec.ts | 21 +------ 5 files changed, 71 insertions(+), 19 deletions(-) diff --git a/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts index ca5a25c..614fed0 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log-transport-config.ts @@ -2,6 +2,11 @@ import { LogLevel } from '../model/log-level.enum.js' export interface ConsoleJsonLogTransportConfig { logLevel: LogLevel + + /** + * function to alter the serialization of log messages. + * @default {@link jsonMapSetStringifyReplacer} + */ jsonStringifyReplacer?: (key: string, value: any) => any /** @@ -16,6 +21,7 @@ export interface ConsoleJsonLogTransportConfig { * when true, the log output will be a JS object instead of a JSON string. {@jsonStringifyReplacer} is still used. * enable this option, when your lambda function is configured with `loggingFormat='JSON'` * @hint when loggingFormat='JSON' is set, you should also configure `applicationLogLevelV2` with `TRACE` - otherwise you won't see all logging output. + * @default false */ logJsObject?: boolean } diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts index 6b44b49..27fc2a4 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts @@ -1,3 +1,4 @@ + import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' import { ConsoleMock, mockConsole, restoreConsole } from '../../test/console-mock.function.js' @@ -242,7 +243,59 @@ describe('handles circular references in log data', () => { }) }) -// New tests for buffering behavior driven by belowLevelLogBufferSize +describe('respects logJsObject config', () => { + let consoleMock: ConsoleMock + let timestamp: Date + + beforeEach(() => { + consoleMock = mockConsole() + timestamp = new Date() + }) + afterEach(restoreConsole) + + test('logs as JSON string when logJsObject is false (default)', () => { + const logger = new ConsoleJsonLogTransport({ + logLevel: LogLevel.WARN, + logJsObject: false, + belowLevelLogBufferSize: 1, + }) + + logger.log(LogLevel.INFO, 'MyClass', '#000', timestamp, ['test message', { data: 'value' }]) + logger.log(LogLevel.WARN, 'MyClass', '#000', timestamp, ['test warn', { data: 'value' }]) + logger.log(LogLevel.ERROR, 'MyClass', '#000', timestamp, ['test error', { data: 'value' }]) + + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + + const warnLog = consoleMock.warn.mock.calls[0][0] + expect(typeof warnLog).toBe('string') + const parsedWarnLog = JSON.parse(warnLog as string) + expect(parsedWarnLog).toEqual(expect.objectContaining({ level: 'WARN', message: 'test warn', })) + + expect(consoleMock.error).toHaveBeenCalledTimes(1) + const errorLog = consoleMock.error.mock.calls[0][0] + expect(typeof errorLog).toBe('string') + const parsedErrorLog = JSON.parse(errorLog as string) + expect(parsedErrorLog).toEqual(expect.objectContaining({ level: 'ERROR', message: 'test error', })) + + }) + + test('logs as JavaScript object when logJsObject is true', () => { + const logger = new ConsoleJsonLogTransport({ + logLevel: LogLevel.ERROR, + logJsObject: true, + belowLevelLogBufferSize: 1, + }) + + logger.log(LogLevel.WARN, 'MyClass', '#000', timestamp, ['test warning', { foo: 'bar' }]) + logger.log(LogLevel.ERROR, 'MyClass', '#000', timestamp, ['test error', { foo: 'bar' }]) + + expect(consoleMock.warn).toHaveBeenCalledTimes(1) + expect(consoleMock.warn.mock.calls[0][0]).toEqual(expect.objectContaining({ level: 'WARN', message: 'test warning' })) + + expect(consoleMock.error).toHaveBeenCalledTimes(1) + expect(consoleMock.error.mock.calls[0][0]).toEqual(expect.objectContaining({ level: 'ERROR', message: 'test error' })) + }) +}) describe('below-level buffering and flush on ERROR', () => { let logger: ConsoleJsonLogTransport diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts index 89111a3..f76cbec 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.ts @@ -58,6 +58,9 @@ export class ConsoleJsonLogTransport extends LogTransport { } protected logToConsole(level: LogLevel, toLog: string) { + if (this.logJsObject) { + toLog = JSON.parse(toLog) + } /* eslint-disable no-console */ switch (level) { case LogLevel.DEBUG: diff --git a/packages/logger/src/node/node-console-log-transport/node-console-log-transport-config.ts b/packages/logger/src/node/node-console-log-transport/node-console-log-transport-config.ts index f2886ae..f4bfa8d 100644 --- a/packages/logger/src/node/node-console-log-transport/node-console-log-transport-config.ts +++ b/packages/logger/src/node/node-console-log-transport/node-console-log-transport-config.ts @@ -4,5 +4,10 @@ import { LogLevel } from '../../model/log-level.enum.js' export interface NodeConsoleLogTransportConfig { logLevel: LogLevel + + /** + * options for {@link util.inspect} when logging objects. + * @default none (uses node.js defaults) + */ nodeInspectOptions?: InspectOptions } diff --git a/packages/logger/src/node/node-console-log-transport/node-console-log-transport.spec.ts b/packages/logger/src/node/node-console-log-transport/node-console-log-transport.spec.ts index 737470f..a1211a3 100644 --- a/packages/logger/src/node/node-console-log-transport/node-console-log-transport.spec.ts +++ b/packages/logger/src/node/node-console-log-transport/node-console-log-transport.spec.ts @@ -5,25 +5,12 @@ import { LogLevel } from '../../model/log-level.enum.js' import { stringToColor } from '../../utils/logger-helper.js' import { NodeConsoleLogTransport } from './node-console-log.transport.js' -/** - * Formats a Date object to HH:mm:ss.SSS format - * @param {Date} date - The date to format - * @returns {string} Formatted time string in HH:mm:ss.SSS format (e.g., "14:23:45.123") - */ -function formatTime(date: Date): string { - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - const seconds = String(date.getSeconds()).padStart(2, '0') - const milliseconds = String(date.getMilliseconds()).padStart(3, '0') - - return `${hours}:${minutes}:${seconds}.${milliseconds}` -} - describe('uses console statement according to levels', () => { + const logDate = Object.freeze(new Date('2020-07-04T12:34:56.789')) + const formattedDate = '12:34:56.789' + let logger: NodeConsoleLogTransport - let logDate: Date let logArgs: any[] - let formattedDate: string let consoleMock: ConsoleMock const className = 'MyClass' const color = stringToColor(className) @@ -31,9 +18,7 @@ describe('uses console statement according to levels', () => { beforeEach(() => { consoleMock = mockConsole() logger = new NodeConsoleLogTransport({ logLevel: LogLevel.DEBUG }) - logDate = new Date() logArgs = ['foo bar'] - formattedDate = formatTime(logDate) }) afterEach(restoreConsole) From 3575396319518e420cb8395996eaa4b725381a04 Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 23 Jan 2026 16:43:34 +0100 Subject: [PATCH 28/29] refactor(console-json-log): actually implement and test logJsObject --- .../console-json-log.transport.spec.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts index 27fc2a4..d64e4f8 100644 --- a/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts +++ b/packages/logger/src/console-json-log-transport/console-json-log.transport.spec.ts @@ -1,4 +1,3 @@ - import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' import { ConsoleMock, mockConsole, restoreConsole } from '../../test/console-mock.function.js' @@ -269,14 +268,13 @@ describe('respects logJsObject config', () => { const warnLog = consoleMock.warn.mock.calls[0][0] expect(typeof warnLog).toBe('string') const parsedWarnLog = JSON.parse(warnLog as string) - expect(parsedWarnLog).toEqual(expect.objectContaining({ level: 'WARN', message: 'test warn', })) + expect(parsedWarnLog).toEqual(expect.objectContaining({ level: 'WARN', message: 'test warn' })) expect(consoleMock.error).toHaveBeenCalledTimes(1) const errorLog = consoleMock.error.mock.calls[0][0] expect(typeof errorLog).toBe('string') const parsedErrorLog = JSON.parse(errorLog as string) - expect(parsedErrorLog).toEqual(expect.objectContaining({ level: 'ERROR', message: 'test error', })) - + expect(parsedErrorLog).toEqual(expect.objectContaining({ level: 'ERROR', message: 'test error' })) }) test('logs as JavaScript object when logJsObject is true', () => { @@ -290,10 +288,14 @@ describe('respects logJsObject config', () => { logger.log(LogLevel.ERROR, 'MyClass', '#000', timestamp, ['test error', { foo: 'bar' }]) expect(consoleMock.warn).toHaveBeenCalledTimes(1) - expect(consoleMock.warn.mock.calls[0][0]).toEqual(expect.objectContaining({ level: 'WARN', message: 'test warning' })) + expect(consoleMock.warn.mock.calls[0][0]).toEqual( + expect.objectContaining({ level: 'WARN', message: 'test warning' }), + ) expect(consoleMock.error).toHaveBeenCalledTimes(1) - expect(consoleMock.error.mock.calls[0][0]).toEqual(expect.objectContaining({ level: 'ERROR', message: 'test error' })) + expect(consoleMock.error.mock.calls[0][0]).toEqual( + expect.objectContaining({ level: 'ERROR', message: 'test error' }), + ) }) }) From 52a6fc1e7cf7a043ec8bbc3a4d3323e5c1222947 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 23 Jan 2026 15:45:49 +0000 Subject: [PATCH 29/29] build(release): next version [skip_build] - @shiftcode/logger@4.0.0-pr250.9 --- package-lock.json | 2 +- packages/logger/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f62a8f..3c8d045 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16753,7 +16753,7 @@ }, "packages/logger": { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.8", + "version": "4.0.0-pr250.9", "license": "UNLICENSED", "devDependencies": { "@shiftcode/utilities": "^4.3.1" diff --git a/packages/logger/package.json b/packages/logger/package.json index b085537..302783c 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/logger", - "version": "4.0.0-pr250.8", + "version": "4.0.0-pr250.9", "description": "logger for local and aws lambda execution", "repository": "https://github.com/shiftcode/sc-commons-public", "license": "UNLICENSED",