cms.c2sgmbh/tests/unit/monitoring/monitoring-logger.unit.spec.ts
Martin Porwoll 0ff8b5c9d8 feat(monitoring): add structured monitoring logger
Fire-and-forget logger that writes to the monitoring-logs collection
with log level filtering via MONITORING_LOG_LEVEL env var. Falls back
to console output when Payload is not yet initialized.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 00:32:09 +00:00

144 lines
4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
const mockCreate = vi.fn().mockResolvedValue({ id: '1' })
const mockGetPayload = vi.fn().mockResolvedValue({ create: mockCreate })
vi.mock('payload', () => ({
getPayload: mockGetPayload,
}))
vi.mock('@payload-config', () => ({
default: {},
}))
import { createMonitoringLogger } from '@/lib/monitoring/monitoring-logger'
/** Wait long enough for fire-and-forget promises to settle. */
async function flush(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 50))
}
describe('MonitoringLogger', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetPayload.mockResolvedValue({ create: mockCreate })
mockCreate.mockResolvedValue({ id: '1' })
delete process.env.MONITORING_LOG_LEVEL
})
afterEach(async () => {
// Drain any pending fire-and-forget promises to prevent cross-test leakage
await flush()
})
it('creates a logger with all five log methods', () => {
const logger = createMonitoringLogger('cron')
expect(logger).toHaveProperty('debug')
expect(logger).toHaveProperty('info')
expect(logger).toHaveProperty('warn')
expect(logger).toHaveProperty('error')
expect(logger).toHaveProperty('fatal')
})
it('writes to payload when log level meets threshold', async () => {
const logger = createMonitoringLogger('cron')
logger.info('Cron job completed', { duration: 3500 })
await flush()
expect(mockGetPayload).toHaveBeenCalled()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
collection: 'monitoring-logs',
data: expect.objectContaining({
level: 'info',
source: 'cron',
message: 'Cron job completed',
duration: 3500,
}),
}),
)
})
it('filters out messages below the configured minimum level', async () => {
process.env.MONITORING_LOG_LEVEL = 'warn'
const logger = createMonitoringLogger('payload')
logger.debug('This should not be logged')
logger.info('This should not be logged either')
await flush()
expect(mockCreate).not.toHaveBeenCalled()
})
it('allows messages at or above the configured minimum level', async () => {
process.env.MONITORING_LOG_LEVEL = 'warn'
const logger = createMonitoringLogger('email')
logger.warn('SMTP timeout')
await flush()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
collection: 'monitoring-logs',
data: expect.objectContaining({
level: 'warn',
source: 'email',
message: 'SMTP timeout',
}),
}),
)
})
it('passes context fields to the payload create call', async () => {
const logger = createMonitoringLogger('sync')
logger.info('Sync completed', { requestId: 'req-123', userId: 42, tenant: 1, duration: 1500 })
await flush()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
requestId: 'req-123',
userId: 42,
tenant: 1,
duration: 1500,
}),
}),
)
})
it('separates extra context fields from known fields', async () => {
const logger = createMonitoringLogger('oauth')
logger.error('Token refresh failed', { userId: 5, provider: 'meta', errorCode: 'EXPIRED' })
await flush()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
userId: 5,
context: { provider: 'meta', errorCode: 'EXPIRED' },
}),
}),
)
})
it('falls back to console when payload is unavailable', async () => {
mockGetPayload.mockRejectedValueOnce(new Error('Not initialized'))
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const logger = createMonitoringLogger('cron')
logger.info('Fallback test')
await flush()
expect(consoleSpy).toHaveBeenCalledWith('[cron][INFO]', 'Fallback test', expect.anything())
consoleSpy.mockRestore()
})
})