Skip to content

Commit

Permalink
feat(logger): update/extend ILogger interface
Browse files Browse the repository at this point in the history
BREAKING CHANGE: update/extend ILogger interface to support
hierarchies of loggers

- update ALogger impl, update ctor
- update ConsoleLogger, MemoryLogger, StreamLogger classes
- update NULL_LOGGER
- add ROOT logger and ProxyLogger class
- add/update docs
- update tests
- update pkg exports
  • Loading branch information
postspectacular committed Feb 16, 2024
1 parent 1714067 commit 887e839
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 37 deletions.
5 changes: 4 additions & 1 deletion packages/logger/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@thi.ng/logger",
"version": "2.1.10",
"description": "Types & basis infrastructure for arbitrary logging (w/ default impls)",
"description": "Basis types for arbitrary & hierarchical logging",
"type": "module",
"module": "./index.js",
"typings": "./index.d.ts",
Expand Down Expand Up @@ -81,6 +81,9 @@
"./null": {
"default": "./null.js"
},
"./root": {
"default": "./root.js"
},
"./stream": {
"default": "./stream.js"
},
Expand Down
52 changes: 37 additions & 15 deletions packages/logger/src/alogger.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,62 @@
import { LogLevel, type ILogger, type LogLevelName } from "./api.js";

import {
LogLevel,
type ILogger,
type LogEntry,
type LogLevelName,
} from "./api.js";

let __nextID = 0;
/**
* Abstract {@link ILogger} base implementation.
*/
export abstract class ALogger implements ILogger {
id: string;
level: LogLevel;

constructor(id: string, level: LogLevel | LogLevelName = LogLevel.FINE) {
this.id = id;
constructor(
id?: string,
level: LogLevel | LogLevelName = LogLevel.FINE,
public parent?: ILogger
) {
this.id = id || `logger-${__nextID++}`;
this.level = typeof level === "string" ? LogLevel[level] : level;
}

addChild(logger: ILogger): ILogger {
logger.parent = this;
return logger;
}

abstract childLogger(id: string, level?: LogLevel): ILogger;

enabled(level: LogLevel) {
return this.level <= level;
}

fine(...args: any[]): void {
this.level <= LogLevel.FINE && this.log(LogLevel.FINE, args);
fine(...args: any[]) {
this.log(LogLevel.FINE, args);
}

debug(...args: any[]) {
this.log(LogLevel.DEBUG, args);
}

debug(...args: any[]): void {
this.level <= LogLevel.DEBUG && this.log(LogLevel.DEBUG, args);
info(...args: any[]) {
this.log(LogLevel.INFO, args);
}

info(...args: any[]): void {
this.level <= LogLevel.INFO && this.log(LogLevel.INFO, args);
warn(...args: any[]) {
this.log(LogLevel.WARN, args);
}

warn(...args: any[]): void {
this.level <= LogLevel.WARN && this.log(LogLevel.WARN, args);
severe(...args: any[]) {
this.log(LogLevel.SEVERE, args);
}

severe(...args: any[]): void {
this.level <= LogLevel.SEVERE && this.log(LogLevel.SEVERE, args);
protected log(level: LogLevel, args: any[]) {
this.level <= level &&
this.logEntry([level, this.id, Date.now(), ...args]);
}

protected abstract log(level: LogLevel, args: any[]): void;
abstract logEntry(entry: LogEntry): void;
}
63 changes: 60 additions & 3 deletions packages/logger/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,79 @@ export type LogLevelName =

export interface ILogger {
/**
* This logger's configured minimum log level
* This logger's configured minimum log level. Any log messages with a lower
* level will not be processed and/or propagated further.
*/
level: LogLevel;

/**
* Parent logger. See {@link ILogger.logEntry}.
*/
parent?: ILogger;
/**
* Returns true if the logger is currently enabled for given `level`.
*
* @param level
*/
enabled(level: LogLevel): boolean;

/**
* Syntax sugar for {@link ILogger.logEntry} and guard to produce a log
* message with level {@link LogLevel.FINE}.
*
* @param args
*/
fine(...args: any[]): void;
/**
* Syntax sugar for {@link ILogger.logEntry} and guard to produce a log
* message with level {@link LogLevel.DEBUG}.
*
* @param args
*/
debug(...args: any[]): void;
/**
* Syntax sugar for {@link ILogger.logEntry} and guard to produce a log
* message with level {@link LogLevel.INFO}.
*
* @param args
*/
info(...args: any[]): void;
/**
* Syntax sugar for {@link ILogger.logEntry} and guard to produce a log
* message with level {@link LogLevel.WARN}.
*
* @param args
*/
warn(...args: any[]): void;
/**
* Syntax sugar for {@link ILogger.logEntry} and guard to produce a log
* message with level {@link LogLevel.SEVERE}.
*
* @param args
*/
severe(...args: any[]): void;
/**
* If this logger has a {@link ILogger.parent}, it will simply forward the
* given log entry to it. Otherwise this method performs the actual logging.
*
* @param e
*/
logEntry(e: LogEntry): void;
/**
* Configures the given logger to become a child of this one (i.e. by
* setting its {@link ILogger.parent} to this instance).
*
* @param logger
*/
addChild(logger: ILogger): ILogger;
/**
* Obtain a new logger instance (usually of same type) with given `id` and
* optional custom log level (default is current log level of this
* instance). The new instance will be configured as child logger for this
* instance.
*
* @param id
* @param level
*/
childLogger(id?: string, level?: LogLevel): ILogger;
}

/**
Expand Down
12 changes: 9 additions & 3 deletions packages/logger/src/console.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { ALogger } from "./alogger.js";
import { LogLevel } from "./api.js";
import { LogLevel, type LogEntry, type ILogger } from "./api.js";
import { expandArgs } from "./utils.js";

/**
* {@link ILogger} implementation writing messages via `console.log`.
*/
export class ConsoleLogger extends ALogger {
protected log(level: LogLevel, args: any[]) {
console.log(`[${LogLevel[level]}] ${this.id}:`, ...expandArgs(args));
childLogger(id?: string, level?: LogLevel): ConsoleLogger {
return new ConsoleLogger(id, level ?? this.level, this);
}

logEntry(e: LogEntry) {
if (e[0] < this.level) return;
console.log(`[${LogLevel[e[0]]}] ${e[1]}:`, ...expandArgs(e.slice(3)));
this.parent && this.parent.logEntry(e);
}
}
1 change: 1 addition & 0 deletions packages/logger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from "./alogger.js";
export * from "./console.js";
export * from "./memory.js";
export * from "./null.js";
export * from "./root.js";
export * from "./stream.js";
export * from "./utils.js";
23 changes: 15 additions & 8 deletions packages/logger/src/memory.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
import { ALogger } from "./alogger.js";
import type { LogEntry, LogLevel, LogLevelName } from "./api.js";
import type { ILogger, LogEntry, LogLevel, LogLevelName } from "./api.js";
import { expandArgs } from "./utils.js";

export class MemoryLogger extends ALogger {
journal: LogEntry[] = [];

constructor(
id: string,
id?: string,
level?: LogLevel | LogLevelName,
parent?: ILogger,
public limit = 1e3
) {
super(id, level);
super(id, level, parent);
}

childLogger(id?: string, level?: LogLevel): MemoryLogger {
return new MemoryLogger(id, level ?? this.level, this, this.limit);
}

logEntry(e: LogEntry) {
if (e[0] < this.level) return;
if (this.journal.length >= this.limit) this.journal.shift();
this.journal.push([e[0], e[1], e[2], ...expandArgs(e.slice(3))]);
this.parent && this.parent.logEntry(e);
}

/**
Expand All @@ -31,9 +43,4 @@ export class MemoryLogger extends ALogger {
messages() {
return this.journal.map((x) => x.slice(3).join(" "));
}

protected log(level: LogLevel, args: any[]) {
if (this.journal.length >= this.limit) this.journal.shift();
this.journal.push([level, this.id, Date.now(), ...expandArgs(args)]);
}
}
5 changes: 5 additions & 0 deletions packages/logger/src/null.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import { LogLevel, type ILogger } from "./api.js";
*/
export const NULL_LOGGER: ILogger = Object.freeze({
level: LogLevel.NONE,
addChild<T extends ILogger>(l: T) {
return l;
},
childLogger: () => NULL_LOGGER,
enabled: () => false,
fine() {},
debug() {},
info() {},
warn() {},
severe() {},
logEntry() {},
});
50 changes: 50 additions & 0 deletions packages/logger/src/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ALogger } from "./alogger.js";
import { LogLevel, type ILogger, type LogEntry } from "./api.js";
import { NULL_LOGGER } from "./null.js";

/**
* A special {@link ILogger} implementation which only proxies its
* {@link ILogger.parent}. Used for {@link ROOT} to allow provision &
* configuration of a single endpoint for an entire tree of child loggers. See
* {@link ROOT} for more details.
*/
export class ProxyLogger extends ALogger {
set(logger: ILogger) {
this.parent = logger;
}

childLogger(id?: string, level?: LogLevel): ProxyLogger {
return new ProxyLogger(id, level ?? this.level, this);
}

logEntry(e: LogEntry) {
this.parent!.logEntry(e);
}
}

/**
* thi.ng/umbrella global root logger, by default configured to not produce any
* output.
*
* @remarks
* Users can attach and create hierarchies of child loggers (each one feeding
* into this root logger) by calling {@link ILogger.childLogger} or
* {@link ILogger.addChild}.
*
* To enable actual (global) logging output for this root logger, use
* `.set()` and provide a {@link ILogger} implementation.
*
* @example
* ```ts
* const myLogger = ROOT.childLogger("custom");
*
* // use console output for root logger
* ROOT.set(new ConsoleLogger("root"));
*
* // log message will be forwarded to root
* myLogger.debug("hello");
*
* // [DEBUG] custom: hello
* ```
*/
export const ROOT = new ProxyLogger("root", LogLevel.FINE, NULL_LOGGER);
24 changes: 18 additions & 6 deletions packages/logger/src/stream.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ALogger } from "./alogger.js";
import { LogLevel, type LogLevelName } from "./api.js";
import {
LogLevel,
type LogEntry,
type LogLevelName,
type ILogger,
} from "./api.js";
import { expandArgsJSON } from "./utils.js";

/**
Expand All @@ -10,15 +15,22 @@ import { expandArgsJSON } from "./utils.js";
export class StreamLogger extends ALogger {
constructor(
protected target: NodeJS.WriteStream,
id: string,
level: LogLevel | LogLevelName = LogLevel.FINE
id?: string,
level?: LogLevel | LogLevelName,
parent?: ILogger
) {
super(id, level);
super(id, level, parent);
}

protected log(level: LogLevel, args: any[]): void {
childLogger(id?: string, level?: LogLevel): StreamLogger {
return new StreamLogger(this.target, id, level ?? this.level, this);
}

logEntry(e: LogEntry) {
if (e[0] < this.level) return;
this.target.write(
`[${LogLevel[level]}] ${this.id}: ${expandArgsJSON(args)}\n`
`[${LogLevel[e[0]]}] ${e[1]}: ${expandArgsJSON(e.slice(3))}\n`
);
this.parent && this.parent.logEntry(e);
}
}
2 changes: 1 addition & 1 deletion packages/logger/test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const journalWithoutTimestamp = (journal: LogEntry[]) =>
});

test("memory", () => {
const logger = new MemoryLogger("test", LogLevel.DEBUG, 3);
const logger = new MemoryLogger("test", LogLevel.DEBUG, undefined, 3);
logger.fine(1, 2, 3);
logger.debug(1, 2, 3);
logger.info([1, 2, 3]);
Expand Down

0 comments on commit 887e839

Please sign in to comment.