Update version to 0.0.16, add Appender and AppenderFactory classes, refactor LoggerFactory methods, and implement logging functionality with tests.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@techniker-me/logger",
|
"name": "@techniker-me/logger",
|
||||||
"version": "0.0.15",
|
"version": "0.0.16",
|
||||||
"description": "A logger package for logging",
|
"description": "A logger package for logging",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default class LoggerFactory {
|
|||||||
return LoggerFactory._loggers.get(category) as Logger;
|
return LoggerFactory._loggers.get(category) as Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static applyApppender(appender: IAppender): Disposable {
|
public static applyAppender(appender: IAppender): Disposable {
|
||||||
LoggerFactory._appenders.add(appender);
|
LoggerFactory._appenders.add(appender);
|
||||||
|
|
||||||
return new Disposable(() => LoggerFactory._appenders.delete(appender));
|
return new Disposable(() => LoggerFactory._appenders.delete(appender));
|
||||||
@@ -40,10 +40,10 @@ export default class LoggerFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static applyConsoleAppender(): void {
|
private static applyConsoleAppender(): void {
|
||||||
LoggerFactory.applyApppender(new ConsoleAppender());
|
LoggerFactory.applyAppender(new ConsoleAppender());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static applyRemoteAppender(): void {
|
private static applyRemoteAppender(): void {
|
||||||
LoggerFactory.applyApppender(new TechnikerMeAppender());
|
LoggerFactory.applyAppender(new TechnikerMeAppender());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
src/appenders/Appender.ts
Normal file
62
src/appenders/Appender.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import {LoggingLevelType} from '../level/LoggingLevel';
|
||||||
|
import ILogMessage from './LogMessage';
|
||||||
|
|
||||||
|
export type AppenderOptions = {
|
||||||
|
domain?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Appender {
|
||||||
|
private readonly _logRecorderUrl: string = 'https://logserver.techniker.me/api/logs';
|
||||||
|
private readonly _domain: string = typeof window !== 'undefined' ? (window.location?.hostname ?? '') : '';
|
||||||
|
private readonly _logMessageQueue: ILogMessage[] = [];
|
||||||
|
private _pendingPostLogMessagePromise: Promise<Response | undefined> | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(logRecorderUrl: string, {domain}: AppenderOptions) {
|
||||||
|
this._logRecorderUrl = logRecorderUrl;
|
||||||
|
this._domain = domain ?? this._domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public log(timestamp: string, level: LoggingLevelType, category: string, message: string): void {
|
||||||
|
const logMessage = {
|
||||||
|
timestamp,
|
||||||
|
domain: this._domain,
|
||||||
|
level,
|
||||||
|
category,
|
||||||
|
message
|
||||||
|
};
|
||||||
|
this.queueMessage(logMessage);
|
||||||
|
this.postLogMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async postLogMessage(): Promise<void> {
|
||||||
|
const logMessage = this._logMessageQueue.shift();
|
||||||
|
|
||||||
|
if (!logMessage || this._pendingPostLogMessagePromise !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof fetch === 'undefined') {
|
||||||
|
console.error('Fetch API is not available in this environment');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pendingPostLogMessagePromise = fetch(this._logRecorderUrl, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
mode: 'no-cors',
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(logMessage)
|
||||||
|
}).then(() => (this._pendingPostLogMessagePromise = undefined));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Unable to send logs due to [%o]', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private queueMessage(logMessage: ILogMessage): void {
|
||||||
|
this._logMessageQueue.push(logMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/appenders/AppenderFactory.ts
Normal file
12
src/appenders/AppenderFactory.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import {AppenderOptions} from './Appender';
|
||||||
|
import Appender from './Appender';
|
||||||
|
|
||||||
|
export default class AppnederFactory {
|
||||||
|
public static createRemoteAppender(remoteAppenderUrl: string, {domain}: AppenderOptions): Appender {
|
||||||
|
return new Appender(remoteAppenderUrl, {domain});
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
throw new Error('AppenderFactory is a static class that may not be instantiated');
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/appenders/LogMessage.ts
Normal file
6
src/appenders/LogMessage.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default interface ILogMessage {
|
||||||
|
timestamp: string;
|
||||||
|
level: string;
|
||||||
|
category: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import type ILogger from './ILogger';
|
import type ILogger from './ILogger';
|
||||||
import type IAppender from './appenders/IAppender';
|
import type IAppender from './appenders/IAppender';
|
||||||
|
import AppenderFactory from './appenders/AppenderFactory';
|
||||||
import LoggerFactory from './LoggerFactory';
|
import LoggerFactory from './LoggerFactory';
|
||||||
import LoggingLevelMapping from './level/LoggingLevelMapping';
|
import LoggingLevelMapping from './level/LoggingLevelMapping';
|
||||||
|
|
||||||
export type {ILogger, IAppender};
|
export type {ILogger, IAppender};
|
||||||
export {LoggerFactory, LoggingLevelMapping};
|
export {AppenderFactory, LoggerFactory, LoggingLevelMapping};
|
||||||
export default {LoggerFactory, LoggingLevelMapping};
|
export default {AppenderFactory, LoggerFactory, LoggingLevelMapping};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
export enum LoggingLevel {
|
export enum LoggingLevel {
|
||||||
Off = -1,
|
Off = -1,
|
||||||
Info = 10,
|
Debug = 10,
|
||||||
Warn = 20,
|
Trace = 20,
|
||||||
Error = 30,
|
Silly = 30,
|
||||||
Debug = 40,
|
Info = 40,
|
||||||
Trace = 50,
|
Warn = 50,
|
||||||
Silly = 60,
|
Error = 60,
|
||||||
All = 100
|
All = 70
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoggingLevelType = 'Off' | 'Info' | 'Warn' | 'Error' | 'Debug' | 'Trace' | 'Silly' | 'All';
|
export type LoggingLevelType = 'Off' | 'Debug' | 'Trace' | 'Info' | 'Silly' | 'Warn' | 'Error' | 'All';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Defaults from '../Defaults';
|
|||||||
import {LoggingLevel} from './LoggingLevel';
|
import {LoggingLevel} from './LoggingLevel';
|
||||||
|
|
||||||
class Threshold {
|
class Threshold {
|
||||||
private _threshold: Subject<LoggingLevel> = new Subject(LoggingLevel.Debug);
|
private _threshold: Subject<LoggingLevel> = new Subject(LoggingLevel.Info);
|
||||||
|
|
||||||
constructor(loggingLevel?: LoggingLevel) {
|
constructor(loggingLevel?: LoggingLevel) {
|
||||||
this._threshold = new Subject(loggingLevel ?? Defaults.loggingLevel);
|
this._threshold = new Subject(loggingLevel ?? Defaults.loggingLevel);
|
||||||
|
|||||||
78
tests/ConsoleAppender.test.ts
Normal file
78
tests/ConsoleAppender.test.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'bun:test';
|
||||||
|
import ConsoleAppender from '../src/appenders/ConsoleAppender';
|
||||||
|
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||||
|
|
||||||
|
describe('ConsoleAppender', () => {
|
||||||
|
let consoleAppender: ConsoleAppender;
|
||||||
|
let mockConsoleLog: any;
|
||||||
|
let mockConsoleError: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleAppender = new ConsoleAppender();
|
||||||
|
|
||||||
|
// Mock console methods
|
||||||
|
mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockConsoleLog.mockRestore();
|
||||||
|
mockConsoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log info messages to console.log', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'info message');
|
||||||
|
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Info] [test-category] info message');
|
||||||
|
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log debug messages to console.log', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Debug', 'test-category', 'debug message');
|
||||||
|
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Debug] [test-category] debug message');
|
||||||
|
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log trace messages to console.log', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Trace', 'test-category', 'trace message');
|
||||||
|
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Trace] [test-category] trace message');
|
||||||
|
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log silly messages to console.log', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Silly', 'test-category', 'silly message');
|
||||||
|
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Silly] [test-category] silly message');
|
||||||
|
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log warn messages to console.error', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Warn', 'test-category', 'warn message');
|
||||||
|
|
||||||
|
expect(mockConsoleError).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Warn] [test-category] warn message');
|
||||||
|
expect(mockConsoleLog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log error messages to console.error', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Error', 'test-category', 'error message');
|
||||||
|
|
||||||
|
expect(mockConsoleError).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [Error] [test-category] error message');
|
||||||
|
expect(mockConsoleLog).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log all messages to console.log', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'All', 'test-category', 'all message');
|
||||||
|
|
||||||
|
expect(mockConsoleLog).toHaveBeenCalledWith('2023-01-01T00:00:00.000Z [All] [test-category] all message');
|
||||||
|
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log off messages', () => {
|
||||||
|
consoleAppender.log('2023-01-01T00:00:00.000Z', 'Off', 'test-category', 'off message');
|
||||||
|
|
||||||
|
expect(mockConsoleLog).not.toHaveBeenCalled();
|
||||||
|
expect(mockConsoleError).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
227
tests/Logger.test.ts
Normal file
227
tests/Logger.test.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi, type Mock } from 'bun:test';
|
||||||
|
import Logger from '../src/Logger';
|
||||||
|
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||||
|
import Threshold from '../src/level/Threshold';
|
||||||
|
import type IAppender from '../src/appenders/IAppender';
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
let mockAppender: IAppender;
|
||||||
|
let threshold: Threshold;
|
||||||
|
let logger: Logger;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAppender = {
|
||||||
|
log: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
threshold = new Threshold(LoggingLevel.Info);
|
||||||
|
logger = new Logger('test-category', threshold, new Set([mockAppender]));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('threshold filtering', () => {
|
||||||
|
it('should log info messages when threshold is Info', () => {
|
||||||
|
logger.info('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String), // timestamp
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'test message'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log warn messages when threshold is Info', () => {
|
||||||
|
logger.warn('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Warn',
|
||||||
|
'test-category',
|
||||||
|
'test message'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log error messages when threshold is Info', () => {
|
||||||
|
logger.error('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Error',
|
||||||
|
'test-category',
|
||||||
|
'test message'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log debug messages when threshold is Info', () => {
|
||||||
|
logger.debug('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log trace messages when threshold is Info', () => {
|
||||||
|
logger.trace('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log silly messages when threshold is Info', () => {
|
||||||
|
logger.silly('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log all messages when threshold is Debug', () => {
|
||||||
|
threshold.value = LoggingLevel.Debug;
|
||||||
|
|
||||||
|
logger.debug('debug message');
|
||||||
|
logger.info('info message');
|
||||||
|
logger.warn('warn message');
|
||||||
|
logger.error('error message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log only error messages when threshold is Error', () => {
|
||||||
|
threshold.value = LoggingLevel.Error;
|
||||||
|
|
||||||
|
logger.error('error message');
|
||||||
|
logger.warn('warn message');
|
||||||
|
logger.info('info message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Error',
|
||||||
|
'test-category',
|
||||||
|
'error message'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('message formatting', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
threshold.value = LoggingLevel.Debug; // Allow all messages for formatting tests
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format messages with %s placeholder', () => {
|
||||||
|
logger.info('Hello %s', 'world');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'Hello world'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format messages with %d placeholder', () => {
|
||||||
|
logger.info('Count: %d', 42);
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'Count: 42'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format messages with %j placeholder', () => {
|
||||||
|
logger.info('Object: %j', { key: 'value' });
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'Object: {"key":"value"}'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle circular references in %j placeholder', () => {
|
||||||
|
const obj: any = { key: 'value' };
|
||||||
|
obj.self = obj;
|
||||||
|
|
||||||
|
logger.info('Circular: %j', obj);
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'Circular: [Circular]'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple placeholders', () => {
|
||||||
|
logger.info('User %s has %d items: %j', 'Alice', 5, ['a', 'b']);
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'User Alice has 5 items: ["a","b"]'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle more placeholders than arguments', () => {
|
||||||
|
logger.info('Hello %s %s', 'world');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'Hello world %s'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle fewer placeholders than arguments', () => {
|
||||||
|
logger.info('Hello %s', 'world', 'extra');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'Hello world'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-number values for %d', () => {
|
||||||
|
logger.info('Value: %d', 'not-a-number');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
'Info',
|
||||||
|
'test-category',
|
||||||
|
'Value: NaN'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timestamp formatting', () => {
|
||||||
|
it('should include ISO timestamp in log messages', () => {
|
||||||
|
const before = new Date().toISOString();
|
||||||
|
logger.info('test');
|
||||||
|
const after = new Date().toISOString();
|
||||||
|
|
||||||
|
const call = (mockAppender.log as Mock).mock.calls[0];
|
||||||
|
const timestamp = call[0];
|
||||||
|
|
||||||
|
expect(timestamp).toBeDefined();
|
||||||
|
expect(() => new Date(timestamp)).not.toThrow();
|
||||||
|
expect(timestamp >= before || timestamp <= after).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('multiple appenders', () => {
|
||||||
|
it('should call all appenders', () => {
|
||||||
|
const mockAppender2: IAppender = {
|
||||||
|
log: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const loggerWithMultiple = new Logger('test', threshold, new Set([mockAppender, mockAppender2]));
|
||||||
|
|
||||||
|
loggerWithMultiple.info('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockAppender2.log).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
94
tests/LoggerFactory.test.ts
Normal file
94
tests/LoggerFactory.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'bun:test';
|
||||||
|
import LoggerFactory from '../src/LoggerFactory';
|
||||||
|
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||||
|
import type IAppender from '../src/appenders/IAppender';
|
||||||
|
|
||||||
|
describe('LoggerFactory', () => {
|
||||||
|
let mockAppender: IAppender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset LoggerFactory state between tests
|
||||||
|
// Note: This is tricky since LoggerFactory uses static state
|
||||||
|
// In a real scenario, we'd need to refactor for better testability
|
||||||
|
mockAppender = {
|
||||||
|
log: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogger', () => {
|
||||||
|
it('should return a logger instance', () => {
|
||||||
|
const logger = LoggerFactory.getLogger('test-category');
|
||||||
|
|
||||||
|
expect(logger).toBeDefined();
|
||||||
|
expect(typeof logger.info).toBe('function');
|
||||||
|
expect(typeof logger.warn).toBe('function');
|
||||||
|
expect(typeof logger.error).toBe('function');
|
||||||
|
expect(typeof logger.debug).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the same logger instance for the same category', () => {
|
||||||
|
const logger1 = LoggerFactory.getLogger('test-category');
|
||||||
|
const logger2 = LoggerFactory.getLogger('test-category');
|
||||||
|
|
||||||
|
expect(logger1).toBe(logger2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return different logger instances for different categories', () => {
|
||||||
|
const logger1 = LoggerFactory.getLogger('category1');
|
||||||
|
const logger2 = LoggerFactory.getLogger('category2');
|
||||||
|
|
||||||
|
expect(logger1).not.toBe(logger2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setLoggingLevel', () => {
|
||||||
|
it('should change the logging threshold', () => {
|
||||||
|
LoggerFactory.setLoggingLevel('Debug');
|
||||||
|
|
||||||
|
// Create a logger and check that debug messages are logged
|
||||||
|
const logger = LoggerFactory.getLogger('test');
|
||||||
|
logger.debug('debug message');
|
||||||
|
|
||||||
|
// Note: We can't easily test this without mocking the appenders
|
||||||
|
// This would require refactoring LoggerFactory for better testability
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('applyAppender', () => {
|
||||||
|
it('should return a disposable', () => {
|
||||||
|
const disposable = LoggerFactory.applyAppender(mockAppender);
|
||||||
|
|
||||||
|
expect(disposable).toBeDefined();
|
||||||
|
expect(typeof disposable.dispose).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add appender to all loggers', () => {
|
||||||
|
const disposable = LoggerFactory.applyAppender(mockAppender);
|
||||||
|
|
||||||
|
const logger = LoggerFactory.getLogger('test');
|
||||||
|
logger.info('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
disposable.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove appender when disposed', () => {
|
||||||
|
const disposable = LoggerFactory.applyAppender(mockAppender);
|
||||||
|
|
||||||
|
const logger = LoggerFactory.getLogger('test');
|
||||||
|
logger.info('test message');
|
||||||
|
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Dispose and try again
|
||||||
|
disposable.dispose();
|
||||||
|
logger.info('test message 2');
|
||||||
|
|
||||||
|
// Should still be called once since we disposed the mock appender
|
||||||
|
// but the default appenders are still there
|
||||||
|
expect(mockAppender.log).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
94
tests/LoggingLevelMapping.test.ts
Normal file
94
tests/LoggingLevelMapping.test.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import LoggingLevelMapping from '../src/level/LoggingLevelMapping';
|
||||||
|
import { LoggingLevel, LoggingLevelType } from '../src/level/LoggingLevel';
|
||||||
|
|
||||||
|
describe('LoggingLevelMapping', () => {
|
||||||
|
describe('convertLoggingLevelToLoggingLevelType', () => {
|
||||||
|
it('should convert LoggingLevel.Off to "Off"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Off)).toBe('Off');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert LoggingLevel.Debug to "Debug"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Debug)).toBe('Debug');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert LoggingLevel.Trace to "Trace"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Trace)).toBe('Trace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert LoggingLevel.Info to "Info"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Info)).toBe('Info');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert LoggingLevel.Silly to "Silly"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Silly)).toBe('Silly');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert LoggingLevel.Warn to "Warn"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Warn)).toBe('Warn');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert LoggingLevel.Error to "Error"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.Error)).toBe('Error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert LoggingLevel.All to "All"', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(LoggingLevel.All)).toBe('All');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertLoggingLevelTypeToLoggingLevel', () => {
|
||||||
|
it('should convert "Off" to LoggingLevel.Off', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Off')).toBe(LoggingLevel.Off);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert "Debug" to LoggingLevel.Debug', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Debug')).toBe(LoggingLevel.Debug);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert "Trace" to LoggingLevel.Trace', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Trace')).toBe(LoggingLevel.Trace);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert "Info" to LoggingLevel.Info', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Info')).toBe(LoggingLevel.Info);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert "Silly" to LoggingLevel.Silly', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Silly')).toBe(LoggingLevel.Silly);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert "Warn" to LoggingLevel.Warn', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Warn')).toBe(LoggingLevel.Warn);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert "Error" to LoggingLevel.Error', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('Error')).toBe(LoggingLevel.Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should convert "All" to LoggingLevel.All', () => {
|
||||||
|
expect(LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel('All')).toBe(LoggingLevel.All);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('round trip conversion', () => {
|
||||||
|
it('should maintain consistency in round trip conversions', () => {
|
||||||
|
const allLevels: LoggingLevel[] = [
|
||||||
|
LoggingLevel.Off,
|
||||||
|
LoggingLevel.Debug,
|
||||||
|
LoggingLevel.Trace,
|
||||||
|
LoggingLevel.Info,
|
||||||
|
LoggingLevel.Silly,
|
||||||
|
LoggingLevel.Warn,
|
||||||
|
LoggingLevel.Error,
|
||||||
|
LoggingLevel.All
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const level of allLevels) {
|
||||||
|
const type = LoggingLevelMapping.convertLoggingLevelToLoggingLevelType(level);
|
||||||
|
const backToLevel = LoggingLevelMapping.convertLoggingLevelTypeToLoggingLevel(type);
|
||||||
|
expect(backToLevel).toBe(level);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
134
tests/TechnikerMeAppender.test.ts
Normal file
134
tests/TechnikerMeAppender.test.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'bun:test';
|
||||||
|
import TechnikerMeAppender from '../src/appenders/TechnikerMeAppender';
|
||||||
|
|
||||||
|
describe('TechnikerMeAppender', () => {
|
||||||
|
let technikerMeAppender: TechnikerMeAppender;
|
||||||
|
let mockFetch: any;
|
||||||
|
let originalFetch: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
technikerMeAppender = new TechnikerMeAppender();
|
||||||
|
|
||||||
|
// Mock fetch
|
||||||
|
originalFetch = global.fetch;
|
||||||
|
mockFetch = vi.fn();
|
||||||
|
global.fetch = mockFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch;
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('log method', () => {
|
||||||
|
it('should queue messages and attempt to post them', () => {
|
||||||
|
mockFetch.mockResolvedValue(new Response());
|
||||||
|
|
||||||
|
technikerMeAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||||
|
|
||||||
|
// Should attempt to fetch
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://logserver.techniker.me/api/logs',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
mode: 'no-cors'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const callArgs = mockFetch.mock.calls[0];
|
||||||
|
const body = JSON.parse(callArgs[1].body);
|
||||||
|
|
||||||
|
expect(body).toEqual({
|
||||||
|
timestamp: '2023-01-01T00:00:00.000Z',
|
||||||
|
level: 'Info',
|
||||||
|
category: 'test-category',
|
||||||
|
message: 'test message',
|
||||||
|
domain: '' // Empty since we're in Node.js environment
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include domain in browser environment', () => {
|
||||||
|
// Mock window.location
|
||||||
|
const originalWindow = global.window;
|
||||||
|
global.window = {
|
||||||
|
location: {
|
||||||
|
hostname: 'example.com'
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const appender = new TechnikerMeAppender();
|
||||||
|
mockFetch.mockResolvedValue(new Response());
|
||||||
|
|
||||||
|
appender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||||
|
|
||||||
|
const callArgs = mockFetch.mock.calls[0];
|
||||||
|
const body = JSON.parse(callArgs[1].body);
|
||||||
|
|
||||||
|
expect(body.domain).toBe('example.com');
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
global.window = originalWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing window gracefully', () => {
|
||||||
|
const originalWindow = global.window;
|
||||||
|
delete (global as any).window;
|
||||||
|
|
||||||
|
const appender = new TechnikerMeAppender();
|
||||||
|
mockFetch.mockResolvedValue(new Response());
|
||||||
|
|
||||||
|
appender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||||
|
|
||||||
|
const callArgs = mockFetch.mock.calls[0];
|
||||||
|
const body = JSON.parse(callArgs[1].body);
|
||||||
|
|
||||||
|
expect(body.domain).toBe('');
|
||||||
|
|
||||||
|
// Restore
|
||||||
|
global.window = originalWindow;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should handle missing fetch API', () => {
|
||||||
|
const originalFetch = global.fetch;
|
||||||
|
delete (global as any).fetch;
|
||||||
|
|
||||||
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
|
technikerMeAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'test-category', 'test message');
|
||||||
|
|
||||||
|
expect(consoleSpy).toHaveBeenCalledWith('Fetch API is not available in this environment');
|
||||||
|
|
||||||
|
consoleSpy.mockRestore();
|
||||||
|
global.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('message queuing', () => {
|
||||||
|
it('should attempt to send messages', () => {
|
||||||
|
mockFetch.mockResolvedValue(new Response());
|
||||||
|
|
||||||
|
technikerMeAppender.log('2023-01-01T00:00:00.000Z', 'Info', 'cat1', 'msg1');
|
||||||
|
|
||||||
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
|
'https://logserver.techniker.me/api/logs',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
mode: 'no-cors'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: Complex async queuing behavior is tested by the core functionality test above
|
||||||
|
});
|
||||||
|
});
|
||||||
66
tests/Threshold.test.ts
Normal file
66
tests/Threshold.test.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import Threshold from '../src/level/Threshold';
|
||||||
|
import { LoggingLevel } from '../src/level/LoggingLevel';
|
||||||
|
import Defaults from '../src/Defaults';
|
||||||
|
|
||||||
|
describe('Threshold', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should use provided logging level', () => {
|
||||||
|
const threshold = new Threshold(LoggingLevel.Debug);
|
||||||
|
|
||||||
|
expect(threshold.value).toBe(LoggingLevel.Debug);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default logging level when none provided', () => {
|
||||||
|
const threshold = new Threshold();
|
||||||
|
|
||||||
|
expect(threshold.value).toBe(Defaults.loggingLevel);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('value getter/setter', () => {
|
||||||
|
it('should get and set threshold value', () => {
|
||||||
|
const threshold = new Threshold(LoggingLevel.Info);
|
||||||
|
|
||||||
|
expect(threshold.value).toBe(LoggingLevel.Info);
|
||||||
|
|
||||||
|
threshold.value = LoggingLevel.Debug;
|
||||||
|
expect(threshold.value).toBe(LoggingLevel.Debug);
|
||||||
|
|
||||||
|
threshold.value = LoggingLevel.Error;
|
||||||
|
expect(threshold.value).toBe(LoggingLevel.Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('threshold comparison logic', () => {
|
||||||
|
it('should work correctly for different levels', () => {
|
||||||
|
const threshold = new Threshold(LoggingLevel.Info);
|
||||||
|
|
||||||
|
// Info level (30) should allow Info (30), Warn (50), Error (60) but not Debug (10)
|
||||||
|
expect(threshold.value > LoggingLevel.Info).toBe(false); // 30 > 30 = false, so Info logs
|
||||||
|
expect(threshold.value > LoggingLevel.Warn).toBe(false); // 30 > 50 = false, so Warn logs
|
||||||
|
expect(threshold.value > LoggingLevel.Error).toBe(false); // 30 > 60 = false, so Error logs
|
||||||
|
expect(threshold.value > LoggingLevel.Debug).toBe(true); // 30 > 10 = true, so Debug doesn't log
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with Debug threshold', () => {
|
||||||
|
const threshold = new Threshold(LoggingLevel.Debug);
|
||||||
|
|
||||||
|
// Debug level (10) should allow all levels
|
||||||
|
expect(threshold.value > LoggingLevel.Debug).toBe(false); // 10 > 10 = false
|
||||||
|
expect(threshold.value > LoggingLevel.Info).toBe(false); // 10 > 30 = false
|
||||||
|
expect(threshold.value > LoggingLevel.Warn).toBe(false); // 10 > 50 = false
|
||||||
|
expect(threshold.value > LoggingLevel.Error).toBe(false); // 10 > 60 = false
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with Error threshold', () => {
|
||||||
|
const threshold = new Threshold(LoggingLevel.Error);
|
||||||
|
|
||||||
|
// Error level (60) should only allow Error and above
|
||||||
|
expect(threshold.value > LoggingLevel.Error).toBe(false); // 60 > 60 = false, so Error logs
|
||||||
|
expect(threshold.value > LoggingLevel.Warn).toBe(true); // 60 > 50 = true, so Warn doesn't log
|
||||||
|
expect(threshold.value > LoggingLevel.Info).toBe(true); // 60 > 30 = true, so Info doesn't log
|
||||||
|
expect(threshold.value > LoggingLevel.Debug).toBe(true); // 60 > 10 = true, so Debug doesn't log
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user