mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 09:31:22 -05:00
feat(std/log): inline and deferred statement resolution logging (#5192)
This commit is contained in:
parent
d0970daacd
commit
81d09ad01c
5 changed files with 375 additions and 38 deletions
|
@ -7,11 +7,11 @@ import * as log from "https://deno.land/std/log/mod.ts";
|
|||
|
||||
// Simple default logger out of the box. You can customize it
|
||||
// by overriding logger and handler named "default", or providing
|
||||
// additional logger configurations
|
||||
// additional logger configurations. You can log any data type.
|
||||
log.debug("Hello world");
|
||||
log.info("Hello world");
|
||||
log.warning("Hello world");
|
||||
log.error("Hello world");
|
||||
log.info(123456);
|
||||
log.warning(true);
|
||||
log.error({ foo: "bar", fizz: "bazz" });
|
||||
log.critical("500 Internal server error");
|
||||
|
||||
// custom configuration with 2 loggers (the default and `tasks` loggers)
|
||||
|
@ -45,12 +45,12 @@ let logger;
|
|||
// get default logger
|
||||
logger = log.getLogger();
|
||||
logger.debug("fizz"); // logs to `console`, because `file` handler requires "WARNING" level
|
||||
logger.warning("buzz"); // logs to both `console` and `file` handlers
|
||||
logger.warning(41256); // logs to both `console` and `file` handlers
|
||||
|
||||
// get custom logger
|
||||
logger = log.getLogger("tasks");
|
||||
logger.debug("fizz"); // won't get output because this logger has "ERROR" level
|
||||
logger.error("buzz"); // log to `console`
|
||||
logger.error({ productType: "book", value: "126.11" }); // log to `console`
|
||||
|
||||
// if you try to use a logger that hasn't been configured
|
||||
// you're good to go, it gets created automatically with level set to 0
|
||||
|
@ -239,3 +239,53 @@ During setup async hooks `setup` and `destroy` are called, you can use them to
|
|||
open and close file/HTTP connection or any other action you might need.
|
||||
|
||||
For examples check source code of `FileHandler` and `TestHandler`.
|
||||
|
||||
### Inline Logging
|
||||
|
||||
Log functions return the data passed in the `msg` parameter. Data is returned
|
||||
regardless if the logger actually logs it.
|
||||
|
||||
```ts
|
||||
const stringData: string = logger.debug("hello world");
|
||||
const booleanData: boolean = logger.debug(true, 1, "abc");
|
||||
const fn = (): number => {
|
||||
return 123;
|
||||
};
|
||||
const resolvedFunctionData: number = logger.debug(fn());
|
||||
console.log(stringData); // 'hello world'
|
||||
console.log(booleanData); // true
|
||||
console.log(resolvedFunctionData); // 123
|
||||
```
|
||||
|
||||
### Lazy Log Evaluation
|
||||
|
||||
Some log statements are expensive to compute. In these cases, you can use lazy
|
||||
log evaluation to prevent the computation taking place if the logger won't log
|
||||
the message.
|
||||
|
||||
```ts
|
||||
// `expensiveFn(5)` is only evaluated if this logger is configured for debug logging
|
||||
logger.debug(() => `this is expensive: ${expensiveFn(5)}`);
|
||||
```
|
||||
|
||||
NOTE: When using lazy log evaluation, `undefined` will be returned if the
|
||||
resolver function is not called because the logger won't log it. E.g.
|
||||
|
||||
```ts
|
||||
await log.setup({
|
||||
handlers: {
|
||||
console: new log.handlers.ConsoleHandler("DEBUG"),
|
||||
},
|
||||
|
||||
loggers: {
|
||||
tasks: {
|
||||
level: "ERROR",
|
||||
handlers: ["console"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// not logged, as debug < error
|
||||
const data: string | undefined = logger.debug(() => someExpenseFn(5, true));
|
||||
console.log(data); // undefined
|
||||
```
|
||||
|
|
|
@ -42,33 +42,99 @@ export class Logger {
|
|||
this.handlers = handlers || [];
|
||||
}
|
||||
|
||||
_log(level: number, msg: string, ...args: unknown[]): void {
|
||||
if (this.level > level) return;
|
||||
/** If the level of the logger is greater than the level to log, then nothing
|
||||
* is logged, otherwise a log record is passed to each log handler. `msg` data
|
||||
* passed in is returned. If a function is passed in, it is only evaluated
|
||||
* if the msg will be logged and the return value will be the result of the
|
||||
* function, not the function itself, unless the function isn't called, in which
|
||||
* case undefined is returned. All types are coerced to strings for logging.
|
||||
*/
|
||||
_log<T>(
|
||||
level: number,
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
if (this.level > level) {
|
||||
return msg instanceof Function ? undefined : msg;
|
||||
}
|
||||
|
||||
const record: LogRecord = new LogRecord(msg, args, level);
|
||||
let fnResult: T | undefined;
|
||||
let logMessage: string;
|
||||
if (msg instanceof Function) {
|
||||
fnResult = msg();
|
||||
logMessage = this.asString(fnResult);
|
||||
} else {
|
||||
logMessage = this.asString(msg);
|
||||
}
|
||||
const record: LogRecord = new LogRecord(logMessage, args, level);
|
||||
|
||||
this.handlers.forEach((handler): void => {
|
||||
handler.handle(record);
|
||||
});
|
||||
|
||||
return msg instanceof Function ? fnResult : msg;
|
||||
}
|
||||
|
||||
debug(msg: string, ...args: unknown[]): void {
|
||||
this._log(LogLevels.DEBUG, msg, ...args);
|
||||
asString(data: unknown): string {
|
||||
if (typeof data === "string") {
|
||||
return data;
|
||||
} else if (
|
||||
data === null ||
|
||||
typeof data === "number" ||
|
||||
typeof data === "bigint" ||
|
||||
typeof data === "boolean" ||
|
||||
typeof data === "undefined" ||
|
||||
typeof data === "symbol"
|
||||
) {
|
||||
return String(data);
|
||||
} else if (typeof data === "object") {
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
return "undefined";
|
||||
}
|
||||
|
||||
info(msg: string, ...args: unknown[]): void {
|
||||
this._log(LogLevels.INFO, msg, ...args);
|
||||
debug<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
debug<T>(msg: T extends Function ? never : T, ...args: unknown[]): T;
|
||||
debug<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
return this._log(LogLevels.DEBUG, msg, ...args);
|
||||
}
|
||||
|
||||
warning(msg: string, ...args: unknown[]): void {
|
||||
this._log(LogLevels.WARNING, msg, ...args);
|
||||
info<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
info<T>(msg: T extends Function ? never : T, ...args: unknown[]): T;
|
||||
info<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
return this._log(LogLevels.INFO, msg, ...args);
|
||||
}
|
||||
|
||||
error(msg: string, ...args: unknown[]): void {
|
||||
this._log(LogLevels.ERROR, msg, ...args);
|
||||
warning<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
warning<T>(msg: T extends Function ? never : T, ...args: unknown[]): T;
|
||||
warning<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
return this._log(LogLevels.WARNING, msg, ...args);
|
||||
}
|
||||
|
||||
critical(msg: string, ...args: unknown[]): void {
|
||||
this._log(LogLevels.CRITICAL, msg, ...args);
|
||||
error<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
error<T>(msg: T extends Function ? never : T, ...args: unknown[]): T;
|
||||
error<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
return this._log(LogLevels.ERROR, msg, ...args);
|
||||
}
|
||||
|
||||
critical<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
critical<T>(msg: T extends Function ? never : T, ...args: unknown[]): T;
|
||||
critical<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
return this._log(LogLevels.CRITICAL, msg, ...args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
const { test } = Deno;
|
||||
import { assertEquals } from "../testing/asserts.ts";
|
||||
import { assertEquals, assert } from "../testing/asserts.ts";
|
||||
import { LogRecord, Logger } from "./logger.ts";
|
||||
import { LogLevels, LevelName } from "./levels.ts";
|
||||
import { BaseHandler } from "./handlers.ts";
|
||||
|
@ -36,7 +36,7 @@ test("customHandler", function (): void {
|
|||
const handler = new TestHandler("DEBUG");
|
||||
const logger = new Logger("DEBUG", [handler]);
|
||||
|
||||
logger.debug("foo", 1, 2);
|
||||
const inlineData: string = logger.debug("foo", 1, 2);
|
||||
|
||||
const record = handler.records[0];
|
||||
assertEquals(record.msg, "foo");
|
||||
|
@ -45,17 +45,23 @@ test("customHandler", function (): void {
|
|||
assertEquals(record.levelName, "DEBUG");
|
||||
|
||||
assertEquals(handler.messages, ["DEBUG foo"]);
|
||||
assertEquals(inlineData!, "foo");
|
||||
});
|
||||
|
||||
test("logFunctions", function (): void {
|
||||
const doLog = (level: LevelName): TestHandler => {
|
||||
const handler = new TestHandler(level);
|
||||
const logger = new Logger(level, [handler]);
|
||||
logger.debug("foo");
|
||||
logger.info("bar");
|
||||
logger.warning("baz");
|
||||
logger.error("boo");
|
||||
logger.critical("doo");
|
||||
const debugData = logger.debug("foo");
|
||||
const infoData = logger.info("bar");
|
||||
const warningData = logger.warning("baz");
|
||||
const errorData = logger.error("boo");
|
||||
const criticalData = logger.critical("doo");
|
||||
assertEquals(debugData, "foo");
|
||||
assertEquals(infoData, "bar");
|
||||
assertEquals(warningData, "baz");
|
||||
assertEquals(errorData, "boo");
|
||||
assertEquals(criticalData, "doo");
|
||||
return handler;
|
||||
};
|
||||
|
||||
|
@ -91,3 +97,125 @@ test("logFunctions", function (): void {
|
|||
|
||||
assertEquals(handler.messages, ["CRITICAL doo"]);
|
||||
});
|
||||
|
||||
test("String resolver fn will not execute if msg will not be logged", function (): void {
|
||||
const handler = new TestHandler("ERROR");
|
||||
const logger = new Logger("ERROR", [handler]);
|
||||
let called = false;
|
||||
|
||||
const expensiveFunction = (): string => {
|
||||
called = true;
|
||||
return "expensive function result";
|
||||
};
|
||||
|
||||
const inlineData: string | undefined = logger.debug(expensiveFunction, 1, 2);
|
||||
assert(!called);
|
||||
assertEquals(inlineData, undefined);
|
||||
});
|
||||
|
||||
test("String resolver fn resolves as expected", function (): void {
|
||||
const handler = new TestHandler("ERROR");
|
||||
const logger = new Logger("ERROR", [handler]);
|
||||
const expensiveFunction = (x: number): string => {
|
||||
return "expensive function result " + x;
|
||||
};
|
||||
|
||||
const firstInlineData = logger.error(() => expensiveFunction(5));
|
||||
const secondInlineData = logger.error(() => expensiveFunction(12), 1, "abc");
|
||||
assertEquals(firstInlineData, "expensive function result 5");
|
||||
assertEquals(secondInlineData, "expensive function result 12");
|
||||
});
|
||||
|
||||
test("All types map correctly to log strings and are returned as is", function (): void {
|
||||
const handler = new TestHandler("DEBUG");
|
||||
const logger = new Logger("DEBUG", [handler]);
|
||||
const sym = Symbol();
|
||||
const syma = Symbol("a");
|
||||
const fn = (): string => {
|
||||
return "abc";
|
||||
};
|
||||
|
||||
// string
|
||||
const data1: string = logger.debug("abc");
|
||||
assertEquals(data1, "abc");
|
||||
const data2: string = logger.debug("def", 1);
|
||||
assertEquals(data2, "def");
|
||||
assertEquals(handler.messages[0], "DEBUG abc");
|
||||
assertEquals(handler.messages[1], "DEBUG def");
|
||||
|
||||
// null
|
||||
const data3: null = logger.info(null);
|
||||
assertEquals(data3, null);
|
||||
const data4: null = logger.info(null, 1);
|
||||
assertEquals(data4, null);
|
||||
assertEquals(handler.messages[2], "INFO null");
|
||||
assertEquals(handler.messages[3], "INFO null");
|
||||
|
||||
// number
|
||||
const data5: number = logger.warning(3);
|
||||
assertEquals(data5, 3);
|
||||
const data6: number = logger.warning(3, 1);
|
||||
assertEquals(data6, 3);
|
||||
assertEquals(handler.messages[4], "WARNING 3");
|
||||
assertEquals(handler.messages[5], "WARNING 3");
|
||||
|
||||
// bigint
|
||||
const data7: bigint = logger.error(5n);
|
||||
assertEquals(data7, 5n);
|
||||
const data8: bigint = logger.error(5n, 1);
|
||||
assertEquals(data8, 5n);
|
||||
assertEquals(handler.messages[6], "ERROR 5");
|
||||
assertEquals(handler.messages[7], "ERROR 5");
|
||||
|
||||
// boolean
|
||||
const data9: boolean = logger.critical(true);
|
||||
assertEquals(data9, true);
|
||||
const data10: boolean = logger.critical(false, 1);
|
||||
assertEquals(data10, false);
|
||||
assertEquals(handler.messages[8], "CRITICAL true");
|
||||
assertEquals(handler.messages[9], "CRITICAL false");
|
||||
|
||||
// undefined
|
||||
const data11: undefined = logger.debug(undefined);
|
||||
assertEquals(data11, undefined);
|
||||
const data12: undefined = logger.debug(undefined, 1);
|
||||
assertEquals(data12, undefined);
|
||||
assertEquals(handler.messages[10], "DEBUG undefined");
|
||||
assertEquals(handler.messages[11], "DEBUG undefined");
|
||||
|
||||
// symbol
|
||||
const data13: symbol = logger.info(sym);
|
||||
assertEquals(data13, sym);
|
||||
const data14: symbol = logger.info(syma, 1);
|
||||
assertEquals(data14, syma);
|
||||
assertEquals(handler.messages[12], "INFO Symbol()");
|
||||
assertEquals(handler.messages[13], "INFO Symbol(a)");
|
||||
|
||||
// function
|
||||
const data15: string | undefined = logger.warning(fn);
|
||||
assertEquals(data15, "abc");
|
||||
const data16: string | undefined = logger.warning(fn, 1);
|
||||
assertEquals(data16, "abc");
|
||||
assertEquals(handler.messages[14], "WARNING abc");
|
||||
assertEquals(handler.messages[15], "WARNING abc");
|
||||
|
||||
// object
|
||||
const data17: { payload: string; other: number } = logger.error({
|
||||
payload: "data",
|
||||
other: 123,
|
||||
});
|
||||
assertEquals(data17, {
|
||||
payload: "data",
|
||||
other: 123,
|
||||
});
|
||||
const data18: { payload: string; other: number } = logger.error(
|
||||
{ payload: "data", other: 123 },
|
||||
1
|
||||
);
|
||||
assertEquals(data18, {
|
||||
payload: "data",
|
||||
other: 123,
|
||||
});
|
||||
assertEquals(handler.messages[16], 'ERROR {"payload":"data","other":123}');
|
||||
assertEquals(handler.messages[17], 'ERROR {"payload":"data","other":123}');
|
||||
});
|
||||
|
|
|
@ -72,16 +72,85 @@ export function getLogger(name?: string): Logger {
|
|||
return result;
|
||||
}
|
||||
|
||||
export const debug = (msg: string, ...args: unknown[]): void =>
|
||||
getLogger("default").debug(msg, ...args);
|
||||
export const info = (msg: string, ...args: unknown[]): void =>
|
||||
getLogger("default").info(msg, ...args);
|
||||
export const warning = (msg: string, ...args: unknown[]): void =>
|
||||
getLogger("default").warning(msg, ...args);
|
||||
export const error = (msg: string, ...args: unknown[]): void =>
|
||||
getLogger("default").error(msg, ...args);
|
||||
export const critical = (msg: string, ...args: unknown[]): void =>
|
||||
getLogger("default").critical(msg, ...args);
|
||||
export function debug<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
export function debug<T>(
|
||||
msg: T extends Function ? never : T,
|
||||
...args: unknown[]
|
||||
): T;
|
||||
export function debug<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
// Assist TS compiler with pass-through generic type
|
||||
if (msg instanceof Function) {
|
||||
return getLogger("default").debug(msg, ...args);
|
||||
}
|
||||
return getLogger("default").debug(msg, ...args);
|
||||
}
|
||||
|
||||
export function info<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
export function info<T>(
|
||||
msg: T extends Function ? never : T,
|
||||
...args: unknown[]
|
||||
): T;
|
||||
export function info<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
// Assist TS compiler with pass-through generic type
|
||||
if (msg instanceof Function) {
|
||||
return getLogger("default").info(msg, ...args);
|
||||
}
|
||||
return getLogger("default").info(msg, ...args);
|
||||
}
|
||||
|
||||
export function warning<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
export function warning<T>(
|
||||
msg: T extends Function ? never : T,
|
||||
...args: unknown[]
|
||||
): T;
|
||||
export function warning<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
// Assist TS compiler with pass-through generic type
|
||||
if (msg instanceof Function) {
|
||||
return getLogger("default").warning(msg, ...args);
|
||||
}
|
||||
return getLogger("default").warning(msg, ...args);
|
||||
}
|
||||
|
||||
export function error<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
export function error<T>(
|
||||
msg: T extends Function ? never : T,
|
||||
...args: unknown[]
|
||||
): T;
|
||||
export function error<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
// Assist TS compiler with pass-through generic type
|
||||
if (msg instanceof Function) {
|
||||
return getLogger("default").error(msg, ...args);
|
||||
}
|
||||
return getLogger("default").error(msg, ...args);
|
||||
}
|
||||
|
||||
export function critical<T>(msg: () => T, ...args: unknown[]): T | undefined;
|
||||
export function critical<T>(
|
||||
msg: T extends Function ? never : T,
|
||||
...args: unknown[]
|
||||
): T;
|
||||
export function critical<T>(
|
||||
msg: (T extends Function ? never : T) | (() => T),
|
||||
...args: unknown[]
|
||||
): T | undefined {
|
||||
// Assist TS compiler with pass-through generic type
|
||||
if (msg instanceof Function) {
|
||||
return getLogger("default").critical(msg, ...args);
|
||||
}
|
||||
return getLogger("default").critical(msg, ...args);
|
||||
}
|
||||
|
||||
export async function setup(config: LogConfig): Promise<void> {
|
||||
state.config = {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
const { test } = Deno;
|
||||
import { assert, assertEquals } from "../testing/asserts.ts";
|
||||
import { getLogger, debug, info, warning, error, critical } from "./mod.ts";
|
||||
import { Logger } from "./logger.ts";
|
||||
import { assert } from "../testing/asserts.ts";
|
||||
import { getLogger } from "./mod.ts";
|
||||
|
||||
let logger: Logger | null = null;
|
||||
try {
|
||||
|
@ -16,3 +16,27 @@ try {
|
|||
test("logger is initialized", function (): void {
|
||||
assert(logger instanceof Logger);
|
||||
});
|
||||
|
||||
test("default loggers work as expected", function (): void {
|
||||
const sym = Symbol("a");
|
||||
const debugData: string = debug("foo");
|
||||
const debugResolver: string | undefined = debug(() => "foo");
|
||||
const infoData: number = info(456, 1, 2, 3);
|
||||
const infoResolver: boolean | undefined = info(() => true);
|
||||
const warningData: symbol = warning(sym);
|
||||
const warningResolver: null | undefined = warning(() => null);
|
||||
const errorData: undefined = error(undefined, 1, 2, 3);
|
||||
const errorResolver: bigint | undefined = error(() => 5n);
|
||||
const criticalData: string = critical("foo");
|
||||
const criticalResolver: string | undefined = critical(() => "bar");
|
||||
assertEquals(debugData, "foo");
|
||||
assertEquals(debugResolver, undefined);
|
||||
assertEquals(infoData, 456);
|
||||
assertEquals(infoResolver, true);
|
||||
assertEquals(warningData, sym);
|
||||
assertEquals(warningResolver, null);
|
||||
assertEquals(errorData, undefined);
|
||||
assertEquals(errorResolver, 5n);
|
||||
assertEquals(criticalData, "foo");
|
||||
assertEquals(criticalResolver, "bar");
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue