From 62e9faac559b51f8018a82febef0a52dd0857053 Mon Sep 17 00:00:00 2001 From: pr0uxx Date: Wed, 23 Sep 2026 23:59:26 +0100 Subject: [PATCH 1/7] ignore jet brains directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9cf7fd2..6731aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,4 @@ dist /bin coverage-output.txt coverage-summary.md +/.idea From 4af95a0e64aba145f367ab9e07b26c0f864e14f5 Mon Sep 17 00:00:00 2001 From: pr0uxx Date: Thu, 24 Sep 2026 00:00:48 +0100 Subject: [PATCH 2/7] Add debug launch profile --- .vscode/launch.json | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..52c9e66 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,36 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Build Watch", + "type": "node-terminal", + "request": "launch", + "command": "npx tspc -p coverage.tsconfig.json --watch" + }, + { + "name": "Vitest Debug", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", + "args": [ + "Logger.test.ts", + "--inspect-brk", + "--no-file-parallelism" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "skipFiles": [ + "/**" + ] + } + ], + "compounds": [ + { + "name": "Watch + Vitest", + "configurations": [ + "Build Watch", + "Vitest Debug" + ] + } + ] +} From 0e345e916e0ba0a63fc6966757a0139a9b06bdf3 Mon Sep 17 00:00:00 2001 From: pr0uxx Date: Thu, 24 Sep 2026 00:01:04 +0100 Subject: [PATCH 3/7] Update reachability pin --- .github/workflows/Security-Reachability.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Security-Reachability.yml b/.github/workflows/Security-Reachability.yml index d2b3c2b..a9558c0 100644 --- a/.github/workflows/Security-Reachability.yml +++ b/.github/workflows/Security-Reachability.yml @@ -84,7 +84,7 @@ jobs: # validate:package:skip-reachability command so workflow validation does not modify its checkout. - name: Install Socket CLI background: true - run: sfw pip install socketsecurity==2.9.2 uv --upgrade + run: sfw pip install socketsecurity==2.9.6 uv --upgrade # Bring job back to sync execution by awaiting for all async jobs to finish before continuing - name: Steps - Convert Back To Synchronous Execution - Packages Updates/Setup From 2aedec82b792850ddd5f27dfc46ed29ebbd3b30c Mon Sep 17 00:00:00 2001 From: pr0uxx Date: Thu, 24 Sep 2026 00:01:34 +0100 Subject: [PATCH 4/7] Add missing license in package.json --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3c71728..5953478 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "name": "Michael Howard", "email": "Michael_Howard@SHI.com" }, + "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/Software-Hardware-Integration-Lab/Log-Engine.git" From 8bdfc5467202a09dbca124ac56b002626e404f19 Mon Sep 17 00:00:00 2001 From: pr0uxx Date: Thu, 24 Sep 2026 00:02:04 +0100 Subject: [PATCH 5/7] Update packages and bump this package version --- package-lock.json | 21 +++++++++++---------- package.json | 8 ++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17950a4..dd2f973 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,23 @@ { "name": "@software-hardware-integration-lab/log-engine", - "version": "0.0.9", + "version": "0.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@software-hardware-integration-lab/log-engine", - "version": "0.0.9", + "version": "0.0.10", "hasInstallScript": true, + "license": "MIT", "dependencies": { "typia": "~12.1.1" }, "devDependencies": { "@azure/storage-blob": "^12.33.0", "@software-hardware-integration-lab/development-utilities": "~3.0.0", - "@types/node": "~26.5.0", + "@types/node": "~26.6.2", "@vitest/coverage-v8": "~5.0.1", - "baseline-browser-mapping": "^2.11.24", + "baseline-browser-mapping": "^2.11.25", "ts-patch": "~4.0.1", "typescript": "~6.0.3", "vitest": "~5.0.1" @@ -1232,9 +1233,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", - "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.2.tgz", + "integrity": "sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1880,9 +1881,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.24", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz", - "integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==", + "version": "2.11.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.25.tgz", + "integrity": "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 5953478..1b3e8b1 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.9", + "version": "0.0.10", "name": "@software-hardware-integration-lab/log-engine", "description": "Logging engine with ability to add plugins for any destination", "author": { @@ -36,7 +36,7 @@ "prepack": "npm run validate:package", "postinstall": "ts-patch install -s" }, - "packageManager": "npm@12.0.2", + "packageManager": "npm@12.1.0", "devEngines": { "runtime": { "name": "node", @@ -54,9 +54,9 @@ "devDependencies": { "@azure/storage-blob": "^12.33.0", "@software-hardware-integration-lab/development-utilities": "~3.0.0", - "@types/node": "~26.5.0", + "@types/node": "~26.6.2", "@vitest/coverage-v8": "~5.0.1", - "baseline-browser-mapping": "^2.11.24", + "baseline-browser-mapping": "^2.11.25", "ts-patch": "~4.0.1", "typescript": "~6.0.3", "vitest": "~5.0.1" From e6a16ef02cfd9f20ced7d9dcc0997f2276ffb8e0 Mon Sep 17 00:00:00 2001 From: pr0uxx Date: Thu, 24 Sep 2026 00:02:47 +0100 Subject: [PATCH 6/7] Add ability to disable timestamp and console log formatting --- src/interfaces/plugins/ConsoleDestination.ts | 13 ++- src/plugins/ConsoleDestination.ts | 65 ++++++++++-- tests/plugins/ConsoleDestination.test.ts | 106 ++++++++++++++++++- 3 files changed, 171 insertions(+), 13 deletions(-) diff --git a/src/interfaces/plugins/ConsoleDestination.ts b/src/interfaces/plugins/ConsoleDestination.ts index 4fbe59c..c37a568 100644 --- a/src/interfaces/plugins/ConsoleDestination.ts +++ b/src/interfaces/plugins/ConsoleDestination.ts @@ -11,12 +11,22 @@ export type ConsoleDestinationLogLevelMap = Partial; + /** + * Flag to dictate if timestamp should be shown on logs. Best turned off if your console already has timestamps. + * Defaults to true. + */ + 'enableTimestamps': boolean; } /** Default level-to-console-method routing used by the console destination. */ @@ -32,5 +42,6 @@ export const DEFAULT_CONSOLE_DESTINATION_LOG_LEVEL_MAP: Record; + + for (const [key, value] of Object.entries(parsed)) { + output += ` | ${ key }: ${ String(value) } `; + } + } catch { + output += ` | additionalContext: ${ log.additionalContext } `; + } + + break; case 'number': + output += ` | additionalContext: ${ log.additionalContext } `; + + break; case 'object': + if (log.additionalContext instanceof Date) { + output += ` | additionalContext: ${ log.additionalContext.toISOString() } `; + } + + break; + case 'undefined': + default: + // Nothing yet + break; + } + + if (log.stack) { + output += `\n${ log.stack }`; + } + + return output.trimEnd(); + } } diff --git a/tests/plugins/ConsoleDestination.test.ts b/tests/plugins/ConsoleDestination.test.ts index 940de4c..57f767b 100644 --- a/tests/plugins/ConsoleDestination.test.ts +++ b/tests/plugins/ConsoleDestination.test.ts @@ -1,6 +1,7 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, assert, describe, expect, it, test, vi } from 'vitest'; import { ConsoleDestination } from '#/plugins/ConsoleDestination.js'; import { LogLevel, type AuditLog, type OperationalLog } from '#/interfaces/LogEngine.js'; +import { ConsoleDestinationOptions } from '../../bin/interfaces/plugins/ConsoleDestination'; const uuid = '00000000-0000-0000-0000-000000000001'; @@ -30,11 +31,107 @@ const audit: AuditLog = { 'userId': 'user' }; +interface FormatTestParams { + 'name': string, + 'log': OperationalLog, + 'expected': string; + 'options'?: ConsoleDestinationOptions; +} + afterEach(() => { vi.restoreAllMocks(); }); +const paramTestString = '2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user'; + describe('ConsoleDestination', () => { + test.each([ + { + 'name': 'should not log undefined additional context', + 'log': operational, + 'expected': paramTestString, + }, + { + 'name': 'should not log timestamp when disabled by options', + 'log': operational, + 'expected': paramTestString.substring(25), + 'options': { enableTimestamps: false } + }, + { + 'name': 'should include request id when defined', + 'log': { ...operational, 'requestId': '00000000-0000-0000-0000-000000000002' }, + 'expected': paramTestString + ' | requestId: 00000000-0000-0000-0000-000000000002' + }, + { + 'name': 'should include tenant id when defined', + 'log': { ...operational, 'tenantId': '00000000-0000-0000-0000-000000000003' }, + 'expected': paramTestString + ' | tenantId: 00000000-0000-0000-0000-000000000003' + }, + { + 'name': 'should include error stack on new line when defined', + 'log': { ...operational, 'stack': 'test error stack' }, + 'expected': paramTestString + '\ntest error stack' + }, + { + 'name': 'should log invalid additional context json object as string', + 'log': { ...operational, 'additionalContext': ' { + const logSpy = vi.spyOn(console, 'warn').mockImplementation(() => void 0); + + const destination = await ConsoleDestination.create(options); + + expect(destination).not.toBeNull(); + + await destination!.log(log); + + expect(logSpy).toHaveBeenCalledTimes(1); + + const actual = logSpy.mock.calls[0][0]; + + expect(actual).toBe(expected); + }); + + it('should log only defined additional context properties', async () => { + const withContext = { + ...operational, + 'additionalContext': JSON.stringify({ + 'bar': void 0, + 'foo': 'hello' + }) + } satisfies OperationalLog; + + const logSpy = vi.spyOn(console, 'warn').mockImplementation(() => void 0); + + const destination = await ConsoleDestination.create(); + + expect(destination).not.toBeNull(); + + await destination!.log(withContext); + + expect(logSpy).toHaveBeenCalledTimes(1); + + const expectedString = [ + '2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user', + ' | foo: hello' + ].join(''); + + const actual = logSpy.mock.calls[0][0]; + + expect(actual).toBe(expectedString); + }); + it('should route operational levels and audit logs to their configured console methods', async () => { const log = vi.spyOn(console, 'log').mockImplementation(() => void 0); @@ -48,7 +145,7 @@ describe('ConsoleDestination', () => { await destination!.auditLog(audit); - expect(warn).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING warning', operational); + expect(warn).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user', void 0); expect(log).toHaveBeenCalledWith('2025-01-02 03:04:05.678: AUDIT : Update changed', audit); }); @@ -60,7 +157,8 @@ describe('ConsoleDestination', () => { const destination = await ConsoleDestination.create({ 'getShouldWriteAuditLogs': () => false, - 'getShouldWriteOperationalLogs': () => false + 'getShouldWriteOperationalLogs': () => false, + 'enableTimestamps': true }); await destination!.log(operational); @@ -79,7 +177,7 @@ describe('ConsoleDestination', () => { await destination!.log(operational); - expect(error).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING warning', operational); + expect(error).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user', void 0); }); it('should dispose without error when no resources are held', async () => { From 0b42a8e91b5cde592451b048633a87a699a723a8 Mon Sep 17 00:00:00 2001 From: pr0uxx Date: Thu, 24 Sep 2026 00:03:29 +0100 Subject: [PATCH 7/7] Add 'Logger' wrapper because I'm sick of writing objects every time I log --- src/Logger.ts | 81 ++++++++++++++++++++++ src/index.ts | 2 + tests/Logger.test.ts | 155 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 src/Logger.ts create mode 100644 tests/Logger.test.ts diff --git a/src/Logger.ts b/src/Logger.ts new file mode 100644 index 0000000..7edc0d3 --- /dev/null +++ b/src/Logger.ts @@ -0,0 +1,81 @@ +import { LogLevel } from './interfaces/LogEngine.js'; +import { LogEngine } from './LogEngine.js'; + +export const Logger = { + /** + * Creates a log message with the `Trace` log level. + * @param message The message to log. + * @param additionalContext An optional additional context object to append to the log. + */ + trace(message: string, additionalContext?: string | number | Date): void { + LogEngine.getInstance().log({ + additionalContext, + 'level': LogLevel.Trace, + message + }); + }, + /** + * Creates a log message with the `Debug` log level. + * @param message The message to log. + * @param additionalContext An optional additional context object to append to the log. + */ + debug(message: string, additionalContext?: string | number | Date): void { + LogEngine.getInstance().log({ + additionalContext, + 'level': LogLevel.Debug, + message + }); + }, + /** + * Creates a log message with the `Information` log level. + * @param message The message to log. + * @param additionalContext An optional additional context object to append to the log. + */ + info(message: string, additionalContext?: string | number | Date): void { + LogEngine.getInstance().log({ + additionalContext, + 'level': LogLevel.Information, + message + }); + }, + /** + * Creates a log message with the `Warn` log level. + * @param message The message to log. + * @param additionalContext An optional additional context object to append to the log. + */ + warn(message: string, additionalContext?: string | number | Date): void { + LogEngine.getInstance().log({ + additionalContext, + 'level': LogLevel.Warning, + message + }); + }, + /** + * Creates a log message with the `Error` log level. + * @param message The message to log. + * @param error The error to log alongside the message. + * @param additionalContext An optional additional context object to append to the log. + */ + error(message: string, error?: Error, additionalContext?: string | number | Date): void { + LogEngine.getInstance().log({ + additionalContext, + 'level': LogLevel.Error, + message, + 'stack': error?.stack + }); + }, + /** + * Creates a log message with the `Critical` log level. + * @param message The message to log. + * @param error The error to log alongside the message. + * @param additionalContext An optional additional context object to append to the log. + */ + crit(message: string, error?: Error, additionalContext?: string | number | Date): void { + LogEngine.getInstance().log({ + additionalContext, + 'level': LogLevel.Critical, + message, + 'stack': error?.stack + }); + } +}; diff --git a/src/index.ts b/src/index.ts index b6e9f07..0c458d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,3 +48,5 @@ export type { } from './interfaces/plugins/AzureStorageDestination.js'; export type { LoggingPluginContract } from './interfaces/plugins/LoggingPlugin.js'; + +export type { Logger } from './Logger.js'; diff --git a/tests/Logger.test.ts b/tests/Logger.test.ts new file mode 100644 index 0000000..0a16d61 --- /dev/null +++ b/tests/Logger.test.ts @@ -0,0 +1,155 @@ +import { describe, expect, it, vi } from 'vitest'; +import { Logger } from '#/Logger.js'; +import { LogEngine } from '../bin/LogEngine'; +import { LogLevel, OperationalLogParameters } from '../bin/interfaces/LogEngine'; + +type LogErrorFunc = (message: string, error?: Error | undefined, additionalContext?: string | number | Date | undefined) => void; +type LogMessageFunc = (message: string, additionalContext?: string | number | Date | undefined) => void; +type TheoryLogFunc = LogErrorFunc | LogMessageFunc; + +interface LoggerTheoryParams { + 'name': string, + 'params': OperationalLogParameters; + 'method': TheoryLogFunc; +} + +describe('Logger', () => { + const message: string = 'message'; + const error: Error = new Error('test error'); + const context: string = 'test additional context'; + + it.each([ + { + 'name': 'Calls log with Critical level, message, error and additional context', + 'params': { + 'additionalContext': context, + 'level': LogLevel.Critical, + message, + 'stack': error.stack + }, + 'method': () => Logger.crit(message, error, context), + }, + { + 'name': 'Calls log with Critical level, message, and error', + 'params': { + 'level': LogLevel.Critical, + message, + 'stack': error.stack + }, + 'method': () => Logger.crit(message, error), + }, + { + 'name': 'Calls log with Critical level and message', + 'params': { + 'level': LogLevel.Critical, + message + }, + 'method': () => Logger.crit(message), + }, + { + 'name': 'Calls log with Error level, message, error and additional context', + 'params': { + 'additionalContext': context, + 'level': LogLevel.Error, + message, + 'stack': error.stack + }, + 'method': () => Logger.error(message, error, context), + }, + { + 'name': 'Calls log with Error level, message, and error', + 'params': { + 'level': LogLevel.Error, + message, + 'stack': error.stack + }, + 'method': () => Logger.error(message, error), + }, + { + 'name': 'Calls log with Error level and message', + 'params': { + 'level': LogLevel.Error, + message + }, + 'method': () => Logger.error(message), + }, + { + 'name': 'Calls log with Warning level, message, and additional context', + 'params': { + 'level': LogLevel.Warning, + message, + 'additionalContext': context + }, + 'method': () => Logger.warn(message, context), + }, + { + 'name': 'Calls log with Warn level and message', + 'params': { + 'level': LogLevel.Warning, + message + }, + 'method': () => Logger.warn(message), + }, + { + 'name': 'Calls log with Information level, message, and additional context', + 'params': { + 'level': LogLevel.Information, + message, + 'additionalContext': context + }, + 'method': () => Logger.info(message, context), + }, + { + 'name': 'Calls log with Information level and message', + 'params': { + 'level': LogLevel.Information, + message + }, + 'method': () => Logger.info(message), + }, + { + 'name': 'Calls log with Debug level, message, and additional context', + 'params': { + 'level': LogLevel.Debug, + message, + 'additionalContext': context + }, + 'method': () => Logger.debug(message, context), + }, + { + 'name': 'Calls log with Debug level and message', + 'params': { + 'level': LogLevel.Debug, + message + }, + 'method': () => Logger.debug(message), + }, + { + 'name': 'Calls log with Trace level, message, and additional context', + 'params': { + 'level': LogLevel.Trace, + message, + 'additionalContext': context + }, + 'method': () => Logger.trace(message, context), + }, + { + 'name': 'Calls log with Trace level and message', + 'params': { + 'level': LogLevel.Trace, + message + }, + 'method': () => Logger.trace(message), + }, + + ] satisfies LoggerTheoryParams[])('[Theory] $name', ({ params, method }) => { + const getInstanceSpy = vi.spyOn(LogEngine, 'getInstance'); + const logSpy = vi.spyOn(LogEngine.prototype, 'log').mockImplementation(() => void 0); + + method(); + + expect(getInstanceSpy).toHaveBeenCalledOnce(); + expect(logSpy).toHaveBeenCalledExactlyOnceWith(params); + + }); +});