0
0
Fork 0
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:
Chris Knight 2020-06-12 14:27:41 +01:00 committed by GitHub
parent d0970daacd
commit 81d09ad01c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 375 additions and 38 deletions

View file

@ -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
```

View file

@ -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);
}
}

View file

@ -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}');
});

View file

@ -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 = {

View file

@ -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");
});