4 Commits

Author SHA1 Message Date
318135e82b Update README based on naming convention change. 2025-06-26 10:58:27 -05:00
ef2b0ed750 Change the naming convention for LogMessage internal message field. 2025-06-26 10:44:57 -05:00
f21cce9944 Update eslint config. 2025-01-07 09:40:09 -06:00
a89a41520c ConsoleLogAppender writes a human-readable summary when logging structured data.
Taking advantage of the new LogMessageFormatter return type, the
ConsoleLogAppender logs the formatted message as-is if it is a string,
but when processing structured data inserts a string summary consisting
of the message level, scope, and message summary or method. The full
object is still logged to the console as well for inspection.
2025-01-07 09:30:21 -06:00
6 changed files with 97 additions and 61 deletions

View File

@ -54,7 +54,7 @@ written to the appender. This requires the assumption of all logs being written
to a text-based destination, which is not always the case for us.
In this library, all logs are internally stored as structured data, defined by
the *LogMessage* interface. Notably, the *message* field can be either a string
the *LogMessage* interface. Notably, the *msg* field can be either a string
or an object, allowing for the addition of arbitrary fields to be added to the
log event.
@ -65,9 +65,9 @@ messages written to the console. the *ApiAppender* would be an example of an
appender for which layout would be irrelevant.
Finally, notice that all of the appenders provided by this library
automatically persist log messages as structured data, flattening the `message`
automatically persist log messages as structured data, flattening the `msg`
field if it is an object. *ConsoleAppender* will write the log message as a
JSON object, promoting fields in the `message` object to top-level fields in
JSON object, promoting fields in the `msg` object to top-level fields in
the output. For example:
```typescript
@ -87,24 +87,24 @@ someBusinessLogic();
results in the following two events logged as output to the console:
```json
{"timestamp":"2021-09-01T00:00:00.000Z","level":"INFO","scope":"example","message":"Starting application"}
{"timestamp":"2021-09-01T00:00:00.000Z","level":"INFO","scope":"example","msg":"Starting application"}
{"timestamp":"2021-09-01T00:00:00.000Z","level":"DEBUG","scope":"example","msg":"Doing some business logic","method":"someBusinessLogic","foo":"bar"}
```
Note that the field name in the structured data for string messages is
"message". In the case of an object message, the fields are used as provided.
"msg". In the case of an object message, the fields are used as provided.
Fields defined on the *LogMessage* interface are reserved and should not be
used as keys in the message object (and will be ignored if they are). So, for
used as keys in the msg object (and will be ignored if they are). So, for
example:
```typescript
logger.debug({ level: 'WARN', message: 'Example of ignored fields', timestamp: 'today' });
logger.debug({ level: 'WARN', msg: 'Example of ignored fields', timestamp: 'today' });
results in the following event logged as output to the console (note the
ignored `level` and `timestamp` fields from the object):
```json
{"timestamp":"2021-09-01T00:00:00.000Z","level":"DEBUG","scope":"example","message":"Example of ignored fields"}
{"timestamp":"2021-09-01T00:00:00.000Z","level":"DEBUG","scope":"example","msg":"Example of ignored fields"}
```
This flattening behavior is implemented in the `flattenMessage` function

View File

@ -25,6 +25,14 @@ const customTypescriptConfig = {
rules: {
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single', { avoidEscape: true }],
semi: ['error', 'never'],
'@typescript-eslint/no-unused-vars': ['error', {
'args': 'all',
'argsIgnorePattern': '^_',
'caughtErrorsIgnorePattern': '^_',
'destructuredArrayIgnorePattern': '^_',
'varsIgnorePattern': '^_',
}],
},
}
@ -51,4 +59,10 @@ export default [
eslintJs.configs.recommended,
...recommendedTypeScriptConfigs,
customTypescriptConfig,
{
rules: {
quotes: ['error', 'single', { avoidEscape: true }],
semi: ['error', 'never'],
},
},
]

View File

@ -1,6 +1,6 @@
{
"name": "@jdbernard/logging",
"version": "2.0.0",
"version": "2.1.0",
"description": "Simple Javascript logging service.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@ -8,7 +8,7 @@
"/dist"
],
"scripts": {
"build": "tsc",
"build": "bunx tsc",
"prepare": "npm run build",
"test": "echo \"Error: no test specified\" && exit 1"
},

View File

@ -3,12 +3,23 @@ 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
* log appender can be configured with a threshold level and a message
* formatter.
*
* When the message formatter returns a string value, that value is logged to
* the console as-is. When the message formatter returns an object, a summary
* string is logged to the console, followed by the object itself. This allows
* logs to be easily read in the console, while still providing the structured
* data for inspection in the browser's developer tools.
*/
export class ConsoleLogAppender implements LogAppender {
public threshold = LogLevel.ALL;
public formatter: LogMessageFormatter = flattenMessage
public formatter: LogMessageFormatter = flattenMessage;
constructor(threshold?: LogLevel, formatter?: LogMessageFormatter) {
if (threshold) {
@ -27,17 +38,13 @@ export class ConsoleLogAppender implements LogAppender {
let logMethod = console.log;
switch (msg.level) {
case LogLevel.ALL:
logMethod = console.log;
break;
case LogLevel.TRACE:
case LogLevel.LOG:
logMethod = console.log;
break;
case LogLevel.DEBUG:
logMethod = console.debug;
break;
case LogLevel.LOG:
logMethod = console.log;
break;
case LogLevel.INFO:
logMethod = console.info;
break;
@ -50,12 +57,25 @@ export class ConsoleLogAppender implements LogAppender {
break;
}
const strMsg = this.formatter(msg);
const fmtMsg = this.formatter(msg);
if (msg.error || msg.stacktrace) {
logMethod(strMsg, msg.error ?? msg.stacktrace);
if (typeof fmtMsg === "string") {
if (msg.error || msg.stacktrace) {
logMethod(fmtMsg, msg.error ?? msg.stacktrace);
} else {
logMethod(fmtMsg);
}
} else {
logMethod(strMsg);
const { message, _error, _stacktrace, ...rest } = fmtMsg;
const summary = `${LogLevel[msg.level]} -- ${msg.scope}: ${
message ?? fmtMsg.method
}\n`;
if (msg.error || msg.stacktrace) {
logMethod(summary, msg.error ?? msg.stacktrace, rest);
} else {
logMethod(summary, rest);
}
}
}
}

View File

@ -1,4 +1,4 @@
import { omit } from './util'
import { omit } from "./util";
export enum LogLevel {
ALL = 0,
@ -25,7 +25,7 @@ export function parseLogLevel(
export interface LogMessage {
scope: string;
level: LogLevel;
message: string | Record<string, unknown>;
msg: string | Record<string, unknown>;
stacktrace?: string;
error?: Error;
timestamp: Date;
@ -53,32 +53,34 @@ export type FlattenedLogMessage = Record<string, unknown>;
* {"scope":"example","level":"INFO","foo":"bar","baz":"qux","timestamp":"2020-01-01T00:00:00.000Z"}
* ```
*/
export function flattenMessage(msg: LogMessage): FlattenedLogMessage {
if (typeof msg.message === 'string') {
return { ...msg, level: LogLevel[msg.level] };
export function flattenMessage(logMsg: LogMessage): FlattenedLogMessage {
if (typeof logMsg.msg === "string") {
return { ...logMsg, level: LogLevel[logMsg.level] };
} else {
const { message, ...rest } = msg;
const { msg, ...rest } = logMsg;
return {
...omit(message, [
'scope',
'level',
'stacktrace',
'error',
'timestamp',
...omit(msg, [
"scope",
"level",
"stacktrace",
"error",
"timestamp",
]),
...rest,
level: LogLevel[msg.level],
level: LogLevel[logMsg.level],
};
}
}
export type LogMessageFormatter = (msg: LogMessage) => string | FlattenedLogMessage;
export type LogMessageFormatter = (
msg: LogMessage,
) => string | FlattenedLogMessage;
export function structuredLogMessageFormatter(msg: LogMessage): string {
return JSON.stringify(flattenMessage(msg));
}
export function simpleTextLogMessageFormatter(msg: LogMessage): string {
return `[${msg.scope}] - ${msg.level}: ${msg.message}`;
return `[${msg.scope}] - ${msg.level}: ${msg.msg}`;
}
export default LogMessage;

View File

@ -55,7 +55,7 @@ export class Logger {
public doLog(
level: LogLevel,
message: Error | MessageType,
msg: Error | MessageType,
stacktrace?: string,
): void {
if (level < this.getEffectiveThreshold()) {
@ -65,57 +65,57 @@ export class Logger {
const logMsg: LogMessage = {
scope: this.name,
level,
message: '',
msg: '',
stacktrace: '',
timestamp: new Date(),
};
if (message === undefined || message === null) {
logMsg.message = message;
if (msg === undefined || msg === null) {
logMsg.msg = msg;
logMsg.stacktrace = stacktrace ?? '';
} else if (message instanceof Error) {
const error = message as Error;
} else if (msg instanceof Error) {
const error = msg as Error;
logMsg.error = error;
logMsg.message = `${error.name}: ${error.message}`;
logMsg.msg = `${error.name}: ${error.message}`;
logMsg.stacktrace = stacktrace ?? error.stack ?? '';
} else if (isDeferredMsg(message)) {
logMsg.message = message();
} else if (isDeferredMsg(msg)) {
logMsg.msg = msg();
logMsg.stacktrace = stacktrace == null ? '' : stacktrace;
} else {
// string | object
logMsg.message = message;
logMsg.msg = msg;
logMsg.stacktrace = stacktrace == null ? '' : stacktrace;
}
this.sendToAppenders(logMsg);
}
public trace(message: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.TRACE, message, stacktrace);
public trace(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.TRACE, msg, stacktrace);
}
public debug(message: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.DEBUG, message, stacktrace);
public debug(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.DEBUG, msg, stacktrace);
}
public log(message: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.LOG, message, stacktrace);
public log(msg: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.LOG, msg, stacktrace);
}
public info(message: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.INFO, message, stacktrace);
public info(msg: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.INFO, msg, stacktrace);
}
public warn(message: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.WARN, message, stacktrace);
public warn(msg: MessageType, stacktrace?: string): void {
this.doLog(LogLevel.WARN, msg, stacktrace);
}
public error(message: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.ERROR, message, stacktrace);
public error(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.ERROR, msg, stacktrace);
}
public fatal(message: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.FATAL, message, stacktrace);
public fatal(msg: Error | MessageType, stacktrace?: string): void {
this.doLog(LogLevel.FATAL, msg, stacktrace);
}
protected sendToAppenders(logMsg: LogMessage) {