5 Commits

Author SHA1 Message Date
jdb f9cb676b46 Properly set default thresholds on log appenders. 2026-05-05 07:50:47 -05:00
jdb 4dcc4fad25 Add BufferLogAppender
Sometimes it is useful to capture log messages in a way that is easy for
the running program to introspect or manually handle.
*BufferLogAppender* captures the logs in a simple array.

Be careful, there is no default flush/clear mechanism that runs
automatically. Users of BufferLogAppender should take care to
render/handle log messages and periodically call `clearBuffer` on the
*BufferLogAppender* instance to avoid the buffer array growing without
limit.
2026-05-05 06:25:09 -05:00
jdb 895a8c42ca ConsoleLogAppender logs TRACE messages with console.log
console.trace generates and prints a full stacktrace for every log
message. This is not what TRACE means in this library. The TRACE level
is intended for logs that are more verbose than you typically want to
log and should only be turned on when you are trying to trace the
detailed behavior of the logged functionality. Printing the full stack
trace with every message makes an already verbose setting exponentially
worse.
2026-01-09 19:23:38 -06:00
jdb 642078e728 Fix all sources to comply with eslint rules introduced in 2.0.0 (no logic changes). 2026-01-09 19:19:15 -06:00
jdb 8b0acc6f40 Add fixed tool versions. 2026-01-09 19:18:56 -06:00
11 changed files with 158 additions and 138 deletions
+3
View File
@@ -0,0 +1,3 @@
[tools]
node = "latest"
deno = "none"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@jdbernard/logging",
"version": "2.3.2",
"version": "2.3.3",
"description": "Simple Javascript logging service.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
+22 -24
View File
@@ -1,32 +1,30 @@
import { LogMessage, LogLevel, flattenMessage, FlattenedLogMessage } from './log-message';
import { LogAppender } from './log-appender';
import { LogMessage, LogLevel, flattenMessage, FlattenedLogMessage } from './log-message'
import { LogAppender } from './log-appender'
export class ApiLogAppender implements LogAppender {
public batchSize = 10;
public minimumTimePassedInSec = 60;
public maximumTimePassedInSec = 120;
public threshold = LogLevel.ALL;
public batchSize = 10
public minimumTimePassedInSec = 60
public maximumTimePassedInSec = 120
public threshold: LogLevel
private msgBuffer: FlattenedLogMessage[] = [];
private lastSent = 0;
private msgBuffer: FlattenedLogMessage[] = []
private lastSent = 0
constructor(
public readonly apiEndpoint: string,
public authToken?: string,
threshold?: LogLevel
) {
setTimeout(this.checkPost, 1000);
if (threshold) {
this.threshold = threshold;
}
setTimeout(this.checkPost, 1000)
this.threshold = threshold ?? LogLevel.ALL
}
public appendMessage(msg: LogMessage): void {
if (this.threshold && msg.level < this.threshold) {
return;
return
}
this.msgBuffer.push(flattenMessage(msg));
this.msgBuffer.push(flattenMessage(msg))
}
private doPost() {
@@ -39,29 +37,29 @@ export class ApiLogAppender implements LogAppender {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.authToken}`
}
});
})
this.lastSent = Date.now();
this.msgBuffer = [];
this.lastSent = Date.now()
this.msgBuffer = []
}
}
private checkPost = () => {
const now = Date.now();
const min = this.lastSent + this.minimumTimePassedInSec * 1000;
const max = this.lastSent + this.maximumTimePassedInSec * 1000;
const now = Date.now()
const min = this.lastSent + this.minimumTimePassedInSec * 1000
const max = this.lastSent + this.maximumTimePassedInSec * 1000
if (
(this.msgBuffer.length >= this.batchSize && min < now) ||
(this.msgBuffer.length > 0 && max < now)
) {
this.doPost();
this.doPost()
}
setTimeout(
this.checkPost,
Math.max(10000, this.minimumTimePassedInSec * 1000)
);
};
)
}
}
export default ApiLogAppender;
export default ApiLogAppender
+21
View File
@@ -0,0 +1,21 @@
import type { LogAppender } from './log-appender'
import { LogLevel, type LogMessage } from './log-message'
export class BufferLogAppender implements LogAppender {
public threshold: LogLevel
public buffer: LogMessage[]
constructor(buffer?: LogMessage[], threshold?: LogLevel) {
this.buffer = buffer ?? []
this.threshold = threshold ?? LogLevel.ALL
}
public appendMessage(msg: LogMessage): void {
if (this.threshold && msg.level < this.threshold) return
else this.buffer.push(msg)
}
public clearBuffer(): void {
this.buffer.length = 0
}
}
+31 -33
View File
@@ -3,8 +3,8 @@ import {
LogLevel,
LogMessage,
LogMessageFormatter,
} from "./log-message";
import { LogAppender } from "./log-appender";
} from './log-message'
import { LogAppender } from './log-appender'
/**
* A log appender that writes log messages to the console. The behavior of the
@@ -18,68 +18,66 @@ import { LogAppender } from "./log-appender";
* data for inspection in the browser's developer tools.
*/
export class ConsoleLogAppender implements LogAppender {
public threshold = LogLevel.ALL;
public formatter: LogMessageFormatter = flattenMessage;
public threshold: LogLevel
public formatter: LogMessageFormatter
constructor(threshold?: LogLevel, formatter?: LogMessageFormatter) {
if (threshold) {
this.threshold = threshold;
}
if (formatter) {
this.formatter = formatter;
}
this.threshold = threshold ?? LogLevel.ALL
this.formatter = formatter ?? flattenMessage
}
public appendMessage(msg: LogMessage): void {
if (this.threshold && msg.level < this.threshold) {
return;
return
}
let logMethod = console.log;
let logMethod = console.log
switch (msg.level) {
case LogLevel.ALL:
case LogLevel.TRACE:
logMethod = console.trace;
break;
case LogLevel.LOG:
logMethod = console.log;
break;
// console.trace generates and prints a stack trace attached to the
// logged message. This is not really what we want for TRACE level
// messages as it just makes things more verbose needlessly, so these
// all log to console.log
logMethod = console.log
break
case LogLevel.DEBUG:
logMethod = console.debug;
break;
logMethod = console.debug
break
case LogLevel.INFO:
logMethod = console.info;
break;
logMethod = console.info
break
case LogLevel.WARN:
logMethod = console.warn;
break;
logMethod = console.warn
break
case LogLevel.ERROR:
case LogLevel.FATAL:
logMethod = console.error;
break;
logMethod = console.error
break
}
const fmtMsg = this.formatter(msg);
const fmtMsg = this.formatter(msg)
if (typeof fmtMsg === "string") {
if (typeof fmtMsg === 'string') {
if (msg.err || msg.stacktrace) {
logMethod(fmtMsg, msg.err ?? msg.stacktrace);
logMethod(fmtMsg, msg.err ?? msg.stacktrace)
} else {
logMethod(fmtMsg);
logMethod(fmtMsg)
}
} else {
const { msg: innerMsg, _err, _stacktrace, ...rest } = fmtMsg;
const { msg: innerMsg, _err, _stacktrace, ...rest } = fmtMsg
const summary = `${LogLevel[msg.level]} -- ${msg.scope}: ${
innerMsg ?? fmtMsg.method
}\n`;
}\n`
if (msg.err || msg.stacktrace) {
logMethod(summary, msg.err ?? msg.stacktrace, rest);
logMethod(summary, msg.err ?? msg.stacktrace, rest)
} else {
logMethod(summary, rest);
logMethod(summary, rest)
}
}
}
}
export default ConsoleLogAppender;
export default ConsoleLogAppender
+6 -6
View File
@@ -1,6 +1,6 @@
export * from './log-message';
export * from './log-appender';
export * from './log-service';
export * from './console-log-appender';
export * from './api-log-appender';
export * from './logger';
export * from './log-message'
export * from './log-appender'
export * from './log-service'
export * from './console-log-appender'
export * from './api-log-appender'
export * from './logger'
+2 -2
View File
@@ -1,8 +1,8 @@
import { LogLevel, LogMessage } from './log-message';
import { LogLevel, LogMessage } from './log-message'
export interface LogAppender {
threshold: LogLevel;
appendMessage(message: LogMessage): void;
}
export default LogAppender;
export default LogAppender
+15 -15
View File
@@ -1,4 +1,4 @@
import { omit } from "./util";
import { omit } from './util'
export enum LogLevel {
ALL = 0,
@@ -16,9 +16,9 @@ export function parseLogLevel(
defaultLevel = LogLevel.INFO,
): LogLevel {
if (str in LogLevel) {
return LogLevel[str as keyof typeof LogLevel] as LogLevel;
return LogLevel[str as keyof typeof LogLevel] as LogLevel
} else {
return defaultLevel;
return defaultLevel
}
}
@@ -57,21 +57,21 @@ export type FlattenedLogMessage = Record<string, unknown>;
* ```
*/
export function flattenMessage(logMsg: LogMessage): FlattenedLogMessage {
if (typeof logMsg.msg === "string") {
return { ...logMsg, level: LogLevel[logMsg.level] };
if (typeof logMsg.msg === 'string') {
return { ...logMsg, level: LogLevel[logMsg.level] }
} else {
const { msg, ...rest } = logMsg;
const { msg, ...rest } = logMsg
return {
...omit(msg, [
"scope",
"level",
"stacktrace",
"err",
"ts",
'scope',
'level',
'stacktrace',
'err',
'ts',
]),
...rest,
level: LogLevel[logMsg.level],
};
}
}
}
export type LogMessageFormatter = (
@@ -79,11 +79,11 @@ export type LogMessageFormatter = (
) => string | FlattenedLogMessage;
export function structuredLogMessageFormatter(msg: LogMessage): string {
return JSON.stringify(flattenMessage(msg));
return JSON.stringify(flattenMessage(msg))
}
export function simpleTextLogMessageFormatter(msg: LogMessage): string {
return `[${msg.scope}] - ${msg.level}: ${msg.msg}`;
return `[${msg.scope}] - ${msg.level}: ${msg.msg}`
}
export default LogMessage;
export default LogMessage
+16 -16
View File
@@ -1,7 +1,7 @@
import { LogLevel } from './log-message';
import { Logger } from './logger';
import { LogLevel } from './log-message'
import { Logger } from './logger'
const ROOT_LOGGER_NAME = 'ROOT';
const ROOT_LOGGER_NAME = 'ROOT'
/**
* Service for managing loggers. A LogService instance defines
@@ -10,19 +10,19 @@ const ROOT_LOGGER_NAME = 'ROOT';
* module.
*/
export class LogService {
private loggers: Record<string, Logger>;
private loggers: Record<string, Logger>
public get ROOT_LOGGER() {
return this.loggers[ROOT_LOGGER_NAME];
return this.loggers[ROOT_LOGGER_NAME]
}
public constructor() {
this.loggers = {};
this.loggers = {}
this.loggers[ROOT_LOGGER_NAME] = new Logger(
ROOT_LOGGER_NAME,
undefined,
LogLevel.ALL,
);
)
}
/**
@@ -55,28 +55,28 @@ export class LogService {
*/
public getLogger(name: string, threshold?: LogLevel): Logger {
if (this.loggers[name]) {
return this.loggers[name];
return this.loggers[name]
}
let parentLogger: Logger;
let parentLogger: Logger
const parentLoggerName = Object.keys(this.loggers)
.filter((n: string) => name.startsWith(n))
.reduce(
(acc: string, cur: string) => (acc.length > cur.length ? acc : cur),
'',
);
)
if (parentLoggerName) {
parentLogger = this.loggers[parentLoggerName];
parentLogger = this.loggers[parentLoggerName]
} else {
parentLogger = this.ROOT_LOGGER;
parentLogger = this.ROOT_LOGGER
}
this.loggers[name] = parentLogger.createChildLogger(name, threshold);
return this.loggers[name];
this.loggers[name] = parentLogger.createChildLogger(name, threshold)
return this.loggers[name]
}
}
export const logService = new LogService();
export default logService;
export const logService = new LogService()
export default logService
+38 -38
View File
@@ -1,11 +1,11 @@
import { LogLevel, LogMessage } from './log-message';
import { LogAppender } from './log-appender';
import { LogLevel, LogMessage } from './log-message'
import { LogAppender } from './log-appender'
export type DeferredMsg = () => string | Record<string, unknown>;
export type MessageType = string | DeferredMsg | Record<string, unknown>;
export function isDeferredMsg(msg: MessageType): msg is DeferredMsg {
return typeof msg === 'function';
return typeof msg === 'function'
}
/**
@@ -41,7 +41,7 @@ export function isDeferredMsg(msg: MessageType): msg is DeferredMsg {
* For more details, see *LogService#getLogger*.
*/
export class Logger {
public appenders: LogAppender[] = [];
public appenders: LogAppender[] = []
public constructor(
public readonly name: string,
@@ -50,7 +50,7 @@ export class Logger {
) {}
public createChildLogger(name: string, threshold?: LogLevel): Logger {
return new Logger(name, this, threshold);
return new Logger(name, this, threshold)
}
public doLog(
@@ -59,7 +59,7 @@ export class Logger {
stacktrace?: string,
): void {
if (level < this.getEffectiveThreshold()) {
return;
return
}
const logMsg: LogMessage = {
@@ -68,77 +68,77 @@ export class Logger {
msg: '',
stacktrace: '',
ts: new Date(),
};
if (msg === undefined || msg === null) {
logMsg.msg = msg;
logMsg.stacktrace = stacktrace ?? '';
} else if (msg instanceof Error) {
const err = msg as Error;
logMsg.err = err;
logMsg.msg = `${err.name}: ${err.message}`;
logMsg.stacktrace = stacktrace ?? err.stack ?? '';
} else if (isDeferredMsg(msg)) {
logMsg.msg = msg();
logMsg.stacktrace = stacktrace == null ? '' : stacktrace;
} else {
// string | object
logMsg.msg = msg;
logMsg.stacktrace = stacktrace == null ? '' : stacktrace;
}
this.sendToAppenders(logMsg);
if (msg === undefined || msg === null) {
logMsg.msg = msg
logMsg.stacktrace = stacktrace ?? ''
} else if (msg instanceof Error) {
const err = msg as Error
logMsg.err = err
logMsg.msg = `${err.name}: ${err.message}`
logMsg.stacktrace = stacktrace ?? err.stack ?? ''
} else if (isDeferredMsg(msg)) {
logMsg.msg = msg()
logMsg.stacktrace = stacktrace == null ? '' : stacktrace
} else {
// string | object
logMsg.msg = msg
logMsg.stacktrace = stacktrace == null ? '' : stacktrace
}
this.sendToAppenders(logMsg)
}
public trace(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.TRACE, msg, stacktrace);
this.doLog(LogLevel.TRACE, msg, stacktrace)
}
public debug(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.DEBUG, msg, stacktrace);
this.doLog(LogLevel.DEBUG, msg, stacktrace)
}
public log(msg: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.LOG, msg, stacktrace);
this.doLog(LogLevel.LOG, msg, stacktrace)
}
public info(msg: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.INFO, msg, stacktrace);
this.doLog(LogLevel.INFO, msg, stacktrace)
}
public warn(msg: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.WARN, msg, stacktrace);
this.doLog(LogLevel.WARN, msg, stacktrace)
}
public error(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.ERROR, msg, stacktrace);
this.doLog(LogLevel.ERROR, msg, stacktrace)
}
public fatal(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.FATAL, msg, stacktrace);
this.doLog(LogLevel.FATAL, msg, stacktrace)
}
protected sendToAppenders(logMsg: LogMessage) {
this.appenders.forEach((app) => {
app.appendMessage(logMsg);
});
app.appendMessage(logMsg)
})
if (this.parentLogger) {
this.parentLogger.sendToAppenders(logMsg);
this.parentLogger.sendToAppenders(logMsg)
}
}
protected getEffectiveThreshold(): LogLevel {
if (this.threshold) {
return this.threshold;
return this.threshold
}
if (this.parentLogger) {
return this.parentLogger.getEffectiveThreshold();
return this.parentLogger.getEffectiveThreshold()
}
// should never happen (root logger should always have a threshold
return LogLevel.ALL;
return LogLevel.ALL
}
}
export default Logger;
export default Logger
+3 -3
View File
@@ -2,11 +2,11 @@ export function omit(
obj: Record<string, unknown>,
keys: string[],
): Record<string, unknown> {
const result: Record<string, unknown> = {};
const result: Record<string, unknown> = {}
for (const key in obj) {
if (!keys.includes(key)) {
result[key] = obj[key];
result[key] = obj[key]
}
}
return result;
return result
}