diff --git a/.github/workflows/Security-Reachability.yml b/.github/workflows/Security-Reachability.yml index a9558c0..996e01b 100644 --- a/.github/workflows/Security-Reachability.yml +++ b/.github/workflows/Security-Reachability.yml @@ -32,7 +32,7 @@ jobs: permissions: issues: write contents: read - pull-requests: read + pull-requests: write # Set of commands to run to compute the reachability of CVEs in the codebase steps: @@ -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.6 uv --upgrade + run: sfw pip install socketsecurity==2.10.0 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 diff --git a/.gitignore b/.gitignore index 6731aa0..782fc40 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,4 @@ dist coverage-output.txt coverage-summary.md /.idea +azurite diff --git a/.npmignore b/.npmignore index 3d5088b..6f3cbf0 100644 --- a/.npmignore +++ b/.npmignore @@ -27,3 +27,7 @@ package-lock.json # Test artifacts that could be emitted into the build directory bin/test .idea + +# Test application +/bin/test-app +/azurite diff --git a/LogEngine.code-workspace b/LogEngine.code-workspace index bffa4ed..0de88b8 100644 --- a/LogEngine.code-workspace +++ b/LogEngine.code-workspace @@ -5,6 +5,7 @@ } ], "settings": { + "azurite.location": "azurite/", "files.associations": { "*.tsconfig.json": "jsonc", "coverage.tsconfig.json": "jsonc", diff --git a/package-lock.json b/package-lock.json index dd2f973..e1719a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@software-hardware-integration-lab/log-engine", - "version": "0.0.10", + "version": "0.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@software-hardware-integration-lab/log-engine", - "version": "0.0.10", + "version": "0.0.11", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1b3e8b1..c6050bd 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.0.10", + "version": "0.0.11", "name": "@software-hardware-integration-lab/log-engine", "description": "Logging engine with ability to add plugins for any destination", "author": { @@ -34,7 +34,8 @@ "validate:package:skip-reachability": "node ./scripts/validate-audit-signatures.ts && npm run lint && npm run coverage && npm run build:prod && node ./scripts/validate-package.ts", "update:reachability-pin": "node ./scripts/update-reachability-pin.ts", "prepack": "npm run validate:package", - "postinstall": "ts-patch install -s" + "postinstall": "ts-patch install -s", + "testapp": "npm run build:prod && node ./bin/test-app/entry-point.js" }, "packageManager": "npm@12.1.0", "devEngines": { diff --git a/src/LogEngine.ts b/src/LogEngine.ts index 5f1472f..abb57a3 100644 --- a/src/LogEngine.ts +++ b/src/LogEngine.ts @@ -304,11 +304,12 @@ export class LogEngine { /** Point in time snapshot of the current enabled plugins to avoid race conditions. */ const pluginsSnapshot = [...this.#pluginList]; - // Iterate through each registered plugin and await its processing of the log entry. - for (const plugin of pluginsSnapshot) { - // Await the plugin operation to process the log entry before moving on to the next plugin, ensuring sequential processing. - await LogEngine.#logErrorHandle(() => plugin.log(logEntry)); - } + /* + * Dispatch to all plugins concurrently; await the batch so callers know every + * plugin has settled before this resolves. + */ + await Promise.all(pluginsSnapshot.map((plugin) => LogEngine + .#logErrorHandle(plugin.id, () => plugin.log(logEntry)))); } /** @@ -323,24 +324,26 @@ export class LogEngine { /** Point in time snapshot of the current enabled plugins to avoid race conditions. */ const pluginsSnapshot = [...this.#pluginList]; - // Iterate through each registered plugin and await its processing of the audit log entry. - for (const plugin of pluginsSnapshot) { - // Await the plugin operation to process the audit log entry before moving on to the next plugin, ensuring sequential processing. - await LogEngine.#logErrorHandle(() => plugin.auditLog(logEntry)); - } + /* + * Dispatch to all plugins concurrently; await the batch so callers know every + * plugin has settled before this resolves. + */ + await Promise.all(pluginsSnapshot.map((plugin) => LogEngine + .#logErrorHandle(plugin.id, () => plugin.auditLog(logEntry)))); } /** * Executes a plugin logging operation and reports any failure without * interrupting delivery to remaining plugins. + * @param pluginId The plugin identifier. * @param logFunction The asynchronous plugin logging operation to execute. * @returns A promise that resolves after the operation completes or its failure is reported. */ - static async #logErrorHandle(logFunction: () => Promise): Promise { + static async #logErrorHandle(pluginId: string, logFunction: () => Promise): Promise { try { await logFunction(); } catch (error: unknown) { - LogEngine.#reportInternalError(`Logging plugin failed: ${ error instanceof Error ? error.message : String(error) }`); + LogEngine.#reportInternalError(`Logging plugin '${ pluginId }' failed: ${ error instanceof Error ? error.message : String(error) }`); } } diff --git a/src/index.ts b/src/index.ts index 0c458d5..952054f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,4 +49,4 @@ export type { export type { LoggingPluginContract } from './interfaces/plugins/LoggingPlugin.js'; -export type { Logger } from './Logger.js'; +export { Logger } from './Logger.js'; diff --git a/src/interfaces/plugins/AzureStorageDestination.ts b/src/interfaces/plugins/AzureStorageDestination.ts index a4440fc..63de4c3 100644 --- a/src/interfaces/plugins/AzureStorageDestination.ts +++ b/src/interfaces/plugins/AzureStorageDestination.ts @@ -36,6 +36,8 @@ export interface AzureStorageDestinationOptions extends LoggingPluginConfigurati 'maxAppendBlockBytes'?: number & tags.Minimum<1>; /** Maximum number of blocks written to a single append blob before rotating to a suffixed blob. */ 'maxBlocksPerBlob'?: number & tags.Minimum<1> & tags.Maximum<50_000>; + /** The minutes duration that a log file will be written to before creating a new log file. */ + 'rotationIntervalMinutes'?: number & tags.Minimum<1> & tags.Maximum<1_440>; } /** Fully resolved options used internally by the Azure Storage destination. */ @@ -44,6 +46,8 @@ export interface ResolvedAzureStorageDestinationOptions extends LoggingPluginCon 'maxAppendBlockBytes': number & tags.Minimum<1>; /** Maximum number of blocks written to a single append blob before rotating to a suffixed blob. */ 'maxBlocksPerBlob': number & tags.Minimum<1> & tags.Maximum<50_000>; + /** The minutes duration that a log file will be written to before creating a new log file. */ + 'rotationIntervalMinutes': number & tags.Minimum<1> & tags.Maximum<1_440>; } /** Static default options for Azure Storage append-blob logging. */ @@ -52,5 +56,7 @@ export const DEFAULT_AZURE_STORAGE_DESTINATION_OPTIONS: ResolvedAzureStorageDest // The stable limit supported by all Append Blob service API versions. 'maxAppendBlockBytes': 4 * 1024 * 1024, // Azure's hard append-blob limit; rotating at this point uses the full available capacity. - 'maxBlocksPerBlob': 50_000 + 'maxBlocksPerBlob': 50_000, + // Default to an hourly log file. + 'rotationIntervalMinutes': 60 }; diff --git a/src/plugins/AzureStorageDestination.ts b/src/plugins/AzureStorageDestination.ts index afe4576..1dd2bcc 100644 --- a/src/plugins/AzureStorageDestination.ts +++ b/src/plugins/AzureStorageDestination.ts @@ -14,7 +14,7 @@ interface PendingLogRecord { interface LogTypeCollection { 'activeBlob': AzureAppendBlobClientLike | undefined; - 'activeBlobDate': number | undefined; + 'activeWindowStart': number | undefined; /** Suffix applied to the blob name when rotating within the same hour due to the block-count limit. */ 'blobSuffix': number; 'blobContainer': AzureBlobContainerLike | undefined; @@ -49,7 +49,7 @@ export class AzureStorageDestination extends LoggingPlugin { this.#operationalCollection = { 'activeBlob': void 0, - 'activeBlobDate': void 0, + 'activeWindowStart': void 0, 'blobContainer': operationalLogContainer, 'blobSuffix': 0, 'blockCount': 0, @@ -59,7 +59,7 @@ export class AzureStorageDestination extends LoggingPlugin { this.#auditCollection = { 'activeBlob': void 0, - 'activeBlobDate': void 0, + 'activeWindowStart': void 0, 'blobContainer': auditLogContainer, 'blobSuffix': 0, 'blockCount': 0, @@ -193,13 +193,13 @@ export class AzureStorageDestination extends LoggingPlugin { if (this.#operationalCollection) { this.#operationalCollection.activeBlob = void 0; - this.#operationalCollection.activeBlobDate = void 0; + this.#operationalCollection.activeWindowStart = void 0; } if (this.#auditCollection) { this.#auditCollection.activeBlob = void 0; - this.#auditCollection.activeBlobDate = void 0; + this.#auditCollection.activeWindowStart = void 0; } }); } @@ -312,20 +312,20 @@ export class AzureStorageDestination extends LoggingPlugin { * @param type Discriminator identifying which collection is being rotated. */ async #ensureActiveBlob(collection: LogTypeCollection, type: 'audit' | 'operational'): Promise { - const activeHour = new Date().setMinutes(0, 0, 0); + const activeWindow = this.#getActiveWindowStart(); - const isNewHour = collection.activeBlobDate === void 0 || activeHour > collection.activeBlobDate; + const isNewWindow = collection.activeWindowStart === void 0 || activeWindow > collection.activeWindowStart; const isBlockLimitReached = collection.blockCount >= this.#appliedOptions.maxBlocksPerBlob; - if (!isNewHour && !isBlockLimitReached) { + if (!isNewWindow && !isBlockLimitReached) { return; } // Reset the suffix on a new hour; otherwise advance it to rotate within the same hour. - let blobSuffix = isNewHour ? 0 : collection.blobSuffix + 1; + let blobSuffix = isNewWindow ? 0 : collection.blobSuffix + 1; - let openedBlob = await this.#openBlob(type, activeHour, blobSuffix); + let openedBlob = await this.#openBlob(type, activeWindow, blobSuffix); let rotationAttempts = 0; @@ -339,14 +339,14 @@ export class AzureStorageDestination extends LoggingPlugin { blobSuffix += 1; - openedBlob = await this.#openBlob(type, activeHour, blobSuffix); + openedBlob = await this.#openBlob(type, activeWindow, blobSuffix); } // eslint-disable-next-line require-atomic-updates -- #ensureActiveBlob only runs within a single-flight flush per collection. collection.activeBlob = openedBlob.blobClient; // eslint-disable-next-line require-atomic-updates -- #ensureActiveBlob only runs within a single-flight flush per collection. - collection.activeBlobDate = activeHour; + collection.activeWindowStart = activeWindow; // eslint-disable-next-line require-atomic-updates -- #ensureActiveBlob only runs within a single-flight flush per collection. collection.blobSuffix = blobSuffix; @@ -355,6 +355,20 @@ export class AzureStorageDestination extends LoggingPlugin { collection.blockCount = openedBlob.committedBlockCount; } + /** + * Calculates the start of the rotation window that the current time falls within, anchored to UTC midnight. + * @returns Millisecond timestamp of the start of the active rotation window. + */ + #getActiveWindowStart(): number { + const now = Date.now(); + + const intervalMs = this.#appliedOptions.rotationIntervalMinutes * 60_000; + + const dayStart = new Date(now).setUTCHours(0, 0, 0, 0); + + return dayStart + (Math.floor((now - dayStart) / intervalMs) * intervalMs); + } + /** * Groups queued records into batches that each fit within the configured byte limit. * @param batch Queued records awaiting a flush. @@ -395,25 +409,23 @@ export class AzureStorageDestination extends LoggingPlugin { * Creates or reopens the append blob for a given hour and suffix, recovering its true committed block * count when it already existed so callers can detect a blob that is already at capacity. * @param type Discriminator identifying which collection is being opened. - * @param activeHour Millisecond timestamp of the top of the hour the blob belongs to. + * @param windowStart Millisecond timestamp of the start of the time window the blob belongs to. * @param suffix Rotation suffix applied when opening a blob other than the first one for the hour. * @returns The opened blob client, if the collection's container is available, alongside its true committed block count. */ - async #openBlob(type: 'audit' | 'operational', activeHour: number, suffix: number): Promise<{ + async #openBlob(type: 'audit' | 'operational', windowStart: number, suffix: number): Promise<{ 'blobClient': AzureAppendBlobClientLike | undefined; 'committedBlockCount': number; }> { - const activeHourDate = new Date(activeHour); - - const isoValue = activeHourDate.toISOString(); + const isoValue = new Date(windowStart).toISOString(); - // Format the blob name based on the current hour. - const datePrefix = `${ isoValue.slice(0, 10) }${ isoValue.slice(11, 13) }`.replaceAll('-', ''); + // YYYYMMDDHHmm — minute resolution is required for sub-hourly rotation intervals. + const datePrefix = `${ isoValue.slice(0, 10).replaceAll('-', '') }${ isoValue.slice(11, 13) }${ isoValue.slice(14, 16) }`; // Append a numeric suffix (e.g. .2, .3) when rotating within the same hour due to the block-count limit. const suffixSegment = suffix > 0 ? `.${ suffix + 1 }` : ''; - // Construct the final blob name in the format YYYYMMDDHH.audit.log or YYYYMMDDHH.operational.log. + // Construct the final blob name in the format YYYYMMDDHHmm.audit.log or YYYYMMDDHHmm.operational.log. const blobName = `${ datePrefix }.${ type }${ suffixSegment }.log`; const collection = type === 'audit' ? this.#auditCollection : this.#operationalCollection; diff --git a/src/plugins/ConsoleDestination.ts b/src/plugins/ConsoleDestination.ts index 749a2c4..b7119ac 100644 --- a/src/plugins/ConsoleDestination.ts +++ b/src/plugins/ConsoleDestination.ts @@ -191,8 +191,13 @@ export class ConsoleDestination extends LoggingPlugin { * @param log The payload to log alongside the message. */ static #writeToConsole(method: ConsoleDestinationMethod, message: string, log?: AuditLog | OperationalLog): void { - // eslint-disable-next-line no-console - console[method](message, log); + if (log) { + // eslint-disable-next-line no-console + console[method](message, log); + } else { + // eslint-disable-next-line no-console + console[method](message); + } } #formatOperationalLog(log: OperationalLog): string { diff --git a/src/test-app/README.md b/src/test-app/README.md new file mode 100644 index 0000000..6bdc030 --- /dev/null +++ b/src/test-app/README.md @@ -0,0 +1 @@ +This app is for long running tests to ensure plugins are robust over time diff --git a/src/test-app/entry-point.ts b/src/test-app/entry-point.ts new file mode 100644 index 0000000..d95de9f --- /dev/null +++ b/src/test-app/entry-point.ts @@ -0,0 +1,169 @@ +/* eslint-disable no-console */ +import { AzureStorageDestination, ConsoleDestination, Logger } from '#/index.js'; +import { LogEngine } from '#/LogEngine.js'; +import { BlobServiceClient, StorageSharedKeyCredential } from '@azure/storage-blob'; +import { setTimeout as delay } from 'node:timers/promises'; +import { performance, monitorEventLoopDelay } from 'node:perf_hooks'; + +interface TestResult { + 'totalErrors': number; + 'totalIterations': number; +} + +type PluginSetup = () => Promise; + +interface MemorySnapshot { + 'timestampMs': number; + 'rssMb': number; + 'heapUsedMb': number; + 'heapTotalMb': number; + 'heapGrowth': number; +} + +const snapshots: MemorySnapshot[] = []; + +const eventLoopMonitor = monitorEventLoopDelay({ + 'resolution': 10 +}); + +let startTime = 0; + +async function testPlugins(durationMs: number, pluginSetups: PluginSetup[]): Promise { + const result: TestResult = { + 'totalErrors': 0, + 'totalIterations': 0 + }; + + await Promise.all(pluginSetups.map((factory) => factory())); + + eventLoopMonitor.enable(); + + startTime = performance.now(); + + const interval = setInterval(() => { + try { + // eslint-disable-next-line no-plusplus + result.totalIterations++; + + Logger.info('logging'); + } catch (error: unknown) { + console.error('BAD THING HAPPENED', error); + + // eslint-disable-next-line no-plusplus + result.totalErrors++; + } + }, 10); + + const metricsInterval = setInterval(() => { + const memory = process.memoryUsage(); + + const startHeap = snapshots[0]?.heapUsedMb; + + const endHeap = snapshots.at(-1)?.heapUsedMb; + + const snapshot = { + 'timestampMs': performance.now(), + 'rssMb': memory.rss / 1024 / 1024, + 'heapUsedMb': memory.heapUsed / 1024 / 1024, + 'heapTotalMb': memory.heapTotal / 1024 / 1024, + 'heapGrowth': (endHeap ?? 0) - (startHeap ?? 0) + }; + + snapshots.push(snapshot); + + console.clear(); + + console.table(snapshot); + }, 10_000); + + Logger.info(`Starting plugin test for ${ durationMs }ms`); + + try { + await delay(durationMs); + } finally { + clearInterval(interval); + + clearInterval(metricsInterval); + + eventLoopMonitor.disable(); + + Logger.info('Plugin test complete'); + } + + return result; +} + +async function setupBlobPlugin(): Promise { + console.log('Setting up blob'); + + const engine = LogEngine.getInstance(); + + const azuriteCredential = new StorageSharedKeyCredential('devstoreaccount1', 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='); + + const blobClient = new BlobServiceClient('http://localhost:10000/devstoreaccount1/', azuriteCredential); + + const containerClient = blobClient.getContainerClient('operational-logs-test'); + + const blobFactory = AzureStorageDestination.create( + containerClient, + void 0, + { + 'getShouldWriteAuditLogs': () => false, + 'getShouldWriteDebugInfo': () => true, + 'getShouldWriteOperationalLogs': () => true, + 'rotationIntervalMinutes': 2, + 'maxBlocksPerBlob': 5, + 'maxAppendBlockBytes': 300 + } + ); + + await engine.addPlugin({ + 'create': () => blobFactory + }); + + console.log('Done setting up blob'); +} + +async function setupConsolePlugin(): Promise { + /** Configure console logging. */ + const consoleDestination = ConsoleDestination.create({ + 'getShouldWriteAuditLogs': () => false, + 'getShouldWriteDebugInfo': () => true, + 'getShouldWriteOperationalLogs': () => true + }); + + await LogEngine.getInstance().addPlugin({ + 'create': () => consoleDestination + }); +} + +console.log('Starting tests'); + +const testDurationMs = (1 / 12) * 60 * 60 * 1000; + +const result = await testPlugins(testDurationMs, [setupConsolePlugin, setupBlobPlugin]); + +const elapsedMs = performance.now() - startTime; + +const averageLogsPerSecond = + result.totalIterations / (elapsedMs / 1000); + +console.log('Complete'); + +console.log(`Total Iterations: ${ result.totalIterations }`); + +console.log(`Total Errors: ${ result.totalErrors }`); + +console.log(`Average logs per second: ${ averageLogsPerSecond }`); + +console.log(`Mean event loop delay ms: ${ (eventLoopMonitor.mean / 1e6).toFixed(2) }`); + +console.log(`p99 event loop delay ms: ${ (eventLoopMonitor.percentile(99) / 1e6).toFixed(2) }`); + +console.log(`Max event loop delay ms: ${ (eventLoopMonitor.max / 1e6).toFixed(2) }`); + +console.log(`peak heap user Mb ${ Math.max(...snapshots.map((snap) => snap.heapUsedMb)) }`); + +console.log(`peak Rss Mb: ${ Math.max(...snapshots.map((snap) => snap.rssMb)) }`); + +console.table(snapshots); diff --git a/tests/LogEngine.test.ts b/tests/LogEngine.test.ts index 891ecf9..b2801d6 100644 --- a/tests/LogEngine.test.ts +++ b/tests/LogEngine.test.ts @@ -220,9 +220,9 @@ describe('LogEngine', () => { expect(failingPlugin.log).toHaveBeenCalledWith(expect.objectContaining({ 'correlationId': UUID_EMPTY })); - expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Logging plugin failed: log failure')); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Logging plugin \'failure\' failed: log failure')); - expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Logging plugin failed: audit failure')); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('Logging plugin \'failure\' failed: audit failure')); LogEngine.configureHost({ 'getRequestMetadata': () => ({ 'correlationId': uuid }) as never, diff --git a/tests/plugins/AzureStorageDestination.test.ts b/tests/plugins/AzureStorageDestination.test.ts index d56a632..827c8a6 100644 --- a/tests/plugins/AzureStorageDestination.test.ts +++ b/tests/plugins/AzureStorageDestination.test.ts @@ -94,8 +94,8 @@ describe('AzureStorageDestination', () => { expect(operationalContainer.createIfNotExists).toHaveBeenCalledOnce(); expect(auditContainer.createIfNotExists).toHaveBeenCalledOnce(); - expect(operationalContainer.blobNames).toEqual(['2025010203.operational.log']); - expect(auditContainer.blobNames).toEqual(['2025010203.audit.log']); + expect(operationalContainer.blobNames).toEqual(['202501020300.operational.log']); + expect(auditContainer.blobNames).toEqual(['202501020300.audit.log']); const [[operationalContent, operationalContentLength]] = operationalContainer.appendBlock.mock.calls as [[string, number]]; const [[auditContent, auditContentLength]] = auditContainer.appendBlock.mock.calls as [[string, number]]; @@ -123,8 +123,8 @@ describe('AzureStorageDestination', () => { await destination.log(operational); expect(operationalContainer.blobNames).toEqual([ - '2025010203.operational.log', - '2025010204.operational.log' + '202501020300.operational.log', + '202501020400.operational.log' ]); expect(operationalContainer.appendBlock).toHaveBeenCalledTimes(3); }); @@ -143,9 +143,9 @@ describe('AzureStorageDestination', () => { await destination.log(operational); expect(operationalContainer.blobNames).toEqual([ - '2025010203.operational.log', - '2025010203.operational.2.log', - '2025010203.operational.3.log' + '202501020300.operational.log', + '202501020300.operational.2.log', + '202501020300.operational.3.log' ]); expect(operationalContainer.appendBlock).toHaveBeenCalledTimes(3); }); @@ -167,8 +167,8 @@ describe('AzureStorageDestination', () => { await destination.log(operational); expect(operationalContainer.blobNames).toEqual([ - '2025010203.operational.log', - '2025010203.operational.2.log' + '202501020300.operational.log', + '202501020300.operational.2.log' ]); }); @@ -192,8 +192,8 @@ describe('AzureStorageDestination', () => { await destination.log(operational); expect(operationalContainer.blobNames).toEqual([ - '2025010203.operational.log', - '2025010203.operational.2.log' + '202501020300.operational.log', + '202501020300.operational.2.log' ]); expect(operationalContainer.appendBlock).toHaveBeenCalledTimes(1); }); @@ -214,8 +214,8 @@ describe('AzureStorageDestination', () => { await destination.log(operational); expect(operationalContainer.blobNames).toEqual([ - '2025010203.operational.log', - '2025010203.operational.2.log' + '202501020300.operational.log', + '202501020300.operational.2.log' ]); }); diff --git a/tests/plugins/ConsoleDestination.test.ts b/tests/plugins/ConsoleDestination.test.ts index 57f767b..595c49a 100644 --- a/tests/plugins/ConsoleDestination.test.ts +++ b/tests/plugins/ConsoleDestination.test.ts @@ -145,7 +145,7 @@ describe('ConsoleDestination', () => { await destination!.auditLog(audit); - expect(warn).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user', void 0); + expect(warn).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user'); expect(log).toHaveBeenCalledWith('2025-01-02 03:04:05.678: AUDIT : Update changed', audit); }); @@ -177,7 +177,7 @@ describe('ConsoleDestination', () => { await destination!.log(operational); - expect(error).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user', void 0); + expect(error).toHaveBeenCalledWith('2025-01-02 03:04:05.678: WARNING | warning | correlationId: 00000000-0000-0000-0000-000000000001 | userId: user'); }); it('should dispose without error when no resources are held', async () => { diff --git a/vitest.config.ts b/vitest.config.ts index bcc5e4d..532c728 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,7 +16,8 @@ export default defineConfig({ 'include': ['bin/**/*.js'], 'exclude': [ 'bin/**/*.d.ts', - 'bin/**/index.js' + 'bin/**/index.js', + 'bin/test-app' ], 'thresholds': coverageThresholds, 'reporter': [