diff options
Diffstat (limited to 'std/log')
m--------- | std | 0 | ||||
-rw-r--r-- | std/log/README.md | 153 | ||||
-rw-r--r-- | std/log/handlers.ts | 120 | ||||
-rw-r--r-- | std/log/handlers_test.ts | 96 | ||||
-rw-r--r-- | std/log/levels.ts | 26 | ||||
-rw-r--r-- | std/log/logger.ts | 65 | ||||
-rw-r--r-- | std/log/logger_test.ts | 93 | ||||
-rw-r--r-- | std/log/mod.ts | 123 | ||||
-rw-r--r-- | std/log/test.ts | 105 |
9 files changed, 781 insertions, 0 deletions
diff --git a/std b/std deleted file mode 160000 -Subproject 43aafbf33285753e7b42230f0eb7969b300f71c diff --git a/std/log/README.md b/std/log/README.md new file mode 100644 index 000000000..613e69922 --- /dev/null +++ b/std/log/README.md @@ -0,0 +1,153 @@ +# Log + +## Usage + +```ts +import * as log from "https://deno.land/std/log/mod.ts"; + +// simple default logger, you can customize it +// by overriding logger and handler named "default" +log.debug("Hello world"); +log.info("Hello world"); +log.warning("Hello world"); +log.error("Hello world"); +log.critical("500 Internal server error"); + +// custom configuration +await log.setup({ + handlers: { + console: new log.handlers.ConsoleHandler("DEBUG"), + + file: new log.handlers.FileHandler("WARNING", { + filename: "./log.txt", + // you can change format of output message + formatter: "{levelName} {msg}" + }) + }, + + loggers: { + // configure default logger available via short-hand methods above + default: { + level: "DEBUG", + handlers: ["console", "file"] + }, + + tasks: { + level: "ERROR", + handlers: ["console"] + } + } +}); + +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 + +// get custom logger +logger = log.getLogger("tasks"); +logger.debug("fizz"); // won't get output becase this logger has "ERROR" level +logger.error("buzz"); // 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 +// so no message is logged +unknownLogger = log.getLogger("mystery"); +unknownLogger.info("foobar"); // no-op +``` + +## Advanced usage + +### Loggers + +Loggers are objects that you interact with. When you use logger method it +constructs a `LogRecord` and passes it down to its handlers for output. To +create custom loggers speficify them in `loggers` when calling `log.setup`. + +#### `LogRecord` + +`LogRecord` is an object that encapsulates provided message and arguments as +well some meta data that can be later used when formatting a message. + +```ts +interface LogRecord { + msg: string; + args: any[]; + datetime: Date; + level: number; + levelName: string; +} +``` + +### Handlers + +Handlers are responsible for actual output of log messages. When handler is +called by logger it firstly checks that `LogRecord`'s level is not lower than +level of the handler. If level check passes, handlers formats log record into +string and outputs it to target. + +`log` module comes with two built-in handlers: + +- `ConsoleHandler` - (default) +- `FileHandler` + +#### Custom message format + +If you want to override default format of message you can define `formatter` +option for handler. It can be either simple string-based format that uses +`LogRecord` fields or more complicated function-based one that takes `LogRecord` +as argument and outputs string. + +Eg. + +```ts +await log.setup({ + handlers: { + stringFmt: new log.handlers.ConsoleHandler("DEBUG", { + formatter: "[{levelName}] {msg}" + }), + + functionFmt: new log.handlers.ConsoleHandler("DEBUG", { + formatter: logRecord => { + let msg = `${logRecord.level} ${logRecord.msg}`; + + logRecord.args.forEach((arg, index) => { + msg += `, arg${index}: ${arg}`; + }); + + return msg; + } + }), + }, + + loggers: { + default: { + level: "DEBUG", + handlers: ["stringFmt", "functionFmt"], + }, + } +}) + +// calling +log.debug("Hello, world!", 1, "two", [3, 4, 5]); +// results in: +[DEBUG] Hello, world! // output from "stringFmt" handler +10 Hello, world!, arg0: 1, arg1: two, arg3: [3, 4, 5] // output from "functionFmt" formatter +``` + +#### Custom handlers + +Custom handlers can be implemented by subclassing `BaseHandler` or +`WriterHandler`. + +`BaseHandler` is bare-bones handler that has no output logic at all, + +`WriterHandler` is an abstract class that supports any target with `Writer` +interface. + +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`. diff --git a/std/log/handlers.ts b/std/log/handlers.ts new file mode 100644 index 000000000..5dfd0caa4 --- /dev/null +++ b/std/log/handlers.ts @@ -0,0 +1,120 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +const { open } = Deno; +type File = Deno.File; +type Writer = Deno.Writer; +import { getLevelByName, LogLevel } from "./levels.ts"; +import { LogRecord } from "./logger.ts"; +import { red, yellow, blue, bold } from "../fmt/colors.ts"; + +const DEFAULT_FORMATTER = "{levelName} {msg}"; +type FormatterFunction = (logRecord: LogRecord) => string; + +interface HandlerOptions { + formatter?: string | FormatterFunction; +} + +export class BaseHandler { + level: number; + levelName: string; + formatter: string | FormatterFunction; + + constructor(levelName: string, options: HandlerOptions = {}) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + + this.formatter = options.formatter || DEFAULT_FORMATTER; + } + + handle(logRecord: LogRecord): void { + if (this.level > logRecord.level) return; + + const msg = this.format(logRecord); + return this.log(msg); + } + + format(logRecord: LogRecord): string { + if (this.formatter instanceof Function) { + return this.formatter(logRecord); + } + + return this.formatter.replace( + /{(\S+)}/g, + (match, p1): string => { + const value = logRecord[p1 as keyof LogRecord]; + + // do not interpolate missing values + if (!value) { + return match; + } + + return String(value); + } + ); + } + + log(_msg: string): void {} + async setup(): Promise<void> {} + async destroy(): Promise<void> {} +} + +export class ConsoleHandler extends BaseHandler { + format(logRecord: LogRecord): string { + let msg = super.format(logRecord); + + switch (logRecord.level) { + case LogLevel.INFO: + msg = blue(msg); + break; + case LogLevel.WARNING: + msg = yellow(msg); + break; + case LogLevel.ERROR: + msg = red(msg); + break; + case LogLevel.CRITICAL: + msg = bold(red(msg)); + break; + default: + break; + } + + return msg; + } + + log(msg: string): void { + console.log(msg); + } +} + +export abstract class WriterHandler extends BaseHandler { + protected _writer!: Writer; + private _encoder = new TextEncoder(); + + log(msg: string): void { + this._writer.write(this._encoder.encode(msg + "\n")); + } +} + +interface FileHandlerOptions extends HandlerOptions { + filename: string; +} + +export class FileHandler extends WriterHandler { + private _file!: File; + private _filename: string; + + constructor(levelName: string, options: FileHandlerOptions) { + super(levelName, options); + this._filename = options.filename; + } + + async setup(): Promise<void> { + // open file in append mode - write only + this._file = await open(this._filename, "a"); + this._writer = this._file; + } + + async destroy(): Promise<void> { + await this._file.close(); + } +} diff --git a/std/log/handlers_test.ts b/std/log/handlers_test.ts new file mode 100644 index 000000000..329541fa1 --- /dev/null +++ b/std/log/handlers_test.ts @@ -0,0 +1,96 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { LogLevel, getLevelName, getLevelByName } from "./levels.ts"; +import { BaseHandler } from "./handlers.ts"; + +class TestHandler extends BaseHandler { + public messages: string[] = []; + + public log(str: string): void { + this.messages.push(str); + } +} + +test(function simpleHandler(): void { + const cases = new Map<number, string[]>([ + [ + LogLevel.DEBUG, + [ + "DEBUG debug-test", + "INFO info-test", + "WARNING warning-test", + "ERROR error-test", + "CRITICAL critical-test" + ] + ], + [ + LogLevel.INFO, + [ + "INFO info-test", + "WARNING warning-test", + "ERROR error-test", + "CRITICAL critical-test" + ] + ], + [ + LogLevel.WARNING, + ["WARNING warning-test", "ERROR error-test", "CRITICAL critical-test"] + ], + [LogLevel.ERROR, ["ERROR error-test", "CRITICAL critical-test"]], + [LogLevel.CRITICAL, ["CRITICAL critical-test"]] + ]); + + for (const [testCase, messages] of cases.entries()) { + const testLevel = getLevelName(testCase); + const handler = new TestHandler(testLevel); + + for (const levelName in LogLevel) { + const level = getLevelByName(levelName); + handler.handle({ + msg: `${levelName.toLowerCase()}-test`, + args: [], + datetime: new Date(), + level: level, + levelName: levelName + }); + } + + assertEquals(handler.level, testCase); + assertEquals(handler.levelName, testLevel); + assertEquals(handler.messages, messages); + } +}); + +test(function testFormatterAsString(): void { + const handler = new TestHandler("DEBUG", { + formatter: "test {levelName} {msg}" + }); + + handler.handle({ + msg: "Hello, world!", + args: [], + datetime: new Date(), + level: LogLevel.DEBUG, + levelName: "DEBUG" + }); + + assertEquals(handler.messages, ["test DEBUG Hello, world!"]); +}); + +test(function testFormatterAsFunction(): void { + const handler = new TestHandler("DEBUG", { + formatter: (logRecord): string => + `fn formmatter ${logRecord.levelName} ${logRecord.msg}` + }); + + handler.handle({ + msg: "Hello, world!", + args: [], + datetime: new Date(), + level: LogLevel.ERROR, + levelName: "ERROR" + }); + + assertEquals(handler.messages, ["fn formmatter ERROR Hello, world!"]); +}); diff --git a/std/log/levels.ts b/std/log/levels.ts new file mode 100644 index 000000000..20cafb205 --- /dev/null +++ b/std/log/levels.ts @@ -0,0 +1,26 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +export const LogLevel: Record<string, number> = { + NOTSET: 0, + DEBUG: 10, + INFO: 20, + WARNING: 30, + ERROR: 40, + CRITICAL: 50 +}; + +const byLevel = { + [LogLevel.NOTSET]: "NOTSET", + [LogLevel.DEBUG]: "DEBUG", + [LogLevel.INFO]: "INFO", + [LogLevel.WARNING]: "WARNING", + [LogLevel.ERROR]: "ERROR", + [LogLevel.CRITICAL]: "CRITICAL" +}; + +export function getLevelByName(name: string): number { + return LogLevel[name]; +} + +export function getLevelName(level: number): string { + return byLevel[level]; +} diff --git a/std/log/logger.ts b/std/log/logger.ts new file mode 100644 index 000000000..7ef96e15a --- /dev/null +++ b/std/log/logger.ts @@ -0,0 +1,65 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { LogLevel, getLevelByName, getLevelName } from "./levels.ts"; +import { BaseHandler } from "./handlers.ts"; + +export interface LogRecord { + msg: string; + args: unknown[]; + datetime: Date; + level: number; + levelName: string; +} + +export class Logger { + level: number; + levelName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handlers: any[]; + + constructor(levelName: string, handlers?: BaseHandler[]) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + + this.handlers = handlers || []; + } + + _log(level: number, msg: string, ...args: unknown[]): void { + if (this.level > level) return; + + // TODO: it'd be a good idea to make it immutable, so + // no handler mangles it by mistake + // TODO: iterpolate msg with values + const record: LogRecord = { + msg: msg, + args: args, + datetime: new Date(), + level: level, + levelName: getLevelName(level) + }; + this.handlers.forEach( + (handler): void => { + handler.handle(record); + } + ); + } + + debug(msg: string, ...args: unknown[]): void { + this._log(LogLevel.DEBUG, msg, ...args); + } + + info(msg: string, ...args: unknown[]): void { + this._log(LogLevel.INFO, msg, ...args); + } + + warning(msg: string, ...args: unknown[]): void { + this._log(LogLevel.WARNING, msg, ...args); + } + + error(msg: string, ...args: unknown[]): void { + this._log(LogLevel.ERROR, msg, ...args); + } + + critical(msg: string, ...args: unknown[]): void { + this._log(LogLevel.CRITICAL, msg, ...args); + } +} diff --git a/std/log/logger_test.ts b/std/log/logger_test.ts new file mode 100644 index 000000000..d8d205c08 --- /dev/null +++ b/std/log/logger_test.ts @@ -0,0 +1,93 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { LogRecord, Logger } from "./logger.ts"; +import { LogLevel } from "./levels.ts"; +import { BaseHandler } from "./handlers.ts"; + +class TestHandler extends BaseHandler { + public messages: string[] = []; + public records: LogRecord[] = []; + + handle(record: LogRecord): void { + this.records.push({ ...record }); + super.handle(record); + } + + public log(str: string): void { + this.messages.push(str); + } +} + +test(function simpleLogger(): void { + const handler = new TestHandler("DEBUG"); + let logger = new Logger("DEBUG"); + + assertEquals(logger.level, LogLevel.DEBUG); + assertEquals(logger.levelName, "DEBUG"); + assertEquals(logger.handlers, []); + + logger = new Logger("DEBUG", [handler]); + + assertEquals(logger.handlers, [handler]); +}); + +test(function customHandler(): void { + const handler = new TestHandler("DEBUG"); + const logger = new Logger("DEBUG", [handler]); + + logger.debug("foo", 1, 2); + + const record = handler.records[0]; + assertEquals(record.msg, "foo"); + assertEquals(record.args, [1, 2]); + assertEquals(record.level, LogLevel.DEBUG); + assertEquals(record.levelName, "DEBUG"); + + assertEquals(handler.messages, ["DEBUG foo"]); +}); + +test(function logFunctions(): void { + const doLog = (level: string): 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"); + return handler; + }; + + let handler: TestHandler; + handler = doLog("DEBUG"); + + assertEquals(handler.messages, [ + "DEBUG foo", + "INFO bar", + "WARNING baz", + "ERROR boo", + "CRITICAL doo" + ]); + + handler = doLog("INFO"); + + assertEquals(handler.messages, [ + "INFO bar", + "WARNING baz", + "ERROR boo", + "CRITICAL doo" + ]); + + handler = doLog("WARNING"); + + assertEquals(handler.messages, ["WARNING baz", "ERROR boo", "CRITICAL doo"]); + + handler = doLog("ERROR"); + + assertEquals(handler.messages, ["ERROR boo", "CRITICAL doo"]); + + handler = doLog("CRITICAL"); + + assertEquals(handler.messages, ["CRITICAL doo"]); +}); diff --git a/std/log/mod.ts b/std/log/mod.ts new file mode 100644 index 000000000..cb166376e --- /dev/null +++ b/std/log/mod.ts @@ -0,0 +1,123 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { Logger } from "./logger.ts"; +import { + BaseHandler, + ConsoleHandler, + WriterHandler, + FileHandler +} from "./handlers.ts"; + +export class LoggerConfig { + level?: string; + handlers?: string[]; +} + +export interface LogConfig { + handlers?: { + [name: string]: BaseHandler; + }; + loggers?: { + [name: string]: LoggerConfig; + }; +} + +const DEFAULT_LEVEL = "INFO"; +const DEFAULT_CONFIG: LogConfig = { + handlers: { + default: new ConsoleHandler(DEFAULT_LEVEL) + }, + + loggers: { + default: { + level: DEFAULT_LEVEL, + handlers: ["default"] + } + } +}; + +const state = { + handlers: new Map<string, BaseHandler>(), + loggers: new Map<string, Logger>(), + config: DEFAULT_CONFIG +}; + +export const handlers = { + BaseHandler, + ConsoleHandler, + WriterHandler, + FileHandler +}; + +export function getLogger(name?: string): Logger { + if (!name) { + return state.loggers.get("default")!; + } + + if (!state.loggers.has(name)) { + const logger = new Logger("NOTSET", []); + state.loggers.set(name, logger); + return logger; + } + + return state.loggers.get(name)!; +} + +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 async function setup(config: LogConfig): Promise<void> { + state.config = { + handlers: { ...DEFAULT_CONFIG.handlers, ...config.handlers }, + loggers: { ...DEFAULT_CONFIG.loggers, ...config.loggers } + }; + + // tear down existing handlers + state.handlers.forEach( + (handler): void => { + handler.destroy(); + } + ); + state.handlers.clear(); + + // setup handlers + const handlers = state.config.handlers || {}; + + for (const handlerName in handlers) { + const handler = handlers[handlerName]; + await handler.setup(); + state.handlers.set(handlerName, handler); + } + + // remove existing loggers + state.loggers.clear(); + + // setup loggers + const loggers = state.config.loggers || {}; + for (const loggerName in loggers) { + const loggerConfig = loggers[loggerName]; + const handlerNames = loggerConfig.handlers || []; + const handlers: BaseHandler[] = []; + + handlerNames.forEach( + (handlerName): void => { + if (state.handlers.has(handlerName)) { + handlers.push(state.handlers.get(handlerName)!); + } + } + ); + + const levelName = loggerConfig.level || DEFAULT_LEVEL; + const logger = new Logger(levelName, handlers); + state.loggers.set(loggerName, logger); + } +} + +setup(DEFAULT_CONFIG); diff --git a/std/log/test.ts b/std/log/test.ts new file mode 100644 index 000000000..5a17f9a35 --- /dev/null +++ b/std/log/test.ts @@ -0,0 +1,105 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import * as log from "./mod.ts"; +import { LogLevel } from "./levels.ts"; + +class TestHandler extends log.handlers.BaseHandler { + public messages: string[] = []; + + log(msg: string): void { + this.messages.push(msg); + } +} + +test(async function defaultHandlers(): Promise<void> { + const loggers: { + [key: string]: (msg: string, ...args: unknown[]) => void; + } = { + DEBUG: log.debug, + INFO: log.info, + WARNING: log.warning, + ERROR: log.error, + CRITICAL: log.critical + }; + + for (const levelName in LogLevel) { + if (levelName === "NOTSET") { + continue; + } + + const logger = loggers[levelName]; + const handler = new TestHandler(levelName); + + await log.setup({ + handlers: { + default: handler + }, + loggers: { + default: { + level: levelName, + handlers: ["default"] + } + } + }); + + logger("foo"); + logger("bar", 1, 2); + + assertEquals(handler.messages, [`${levelName} foo`, `${levelName} bar`]); + } +}); + +test(async function getLogger(): Promise<void> { + const handler = new TestHandler("DEBUG"); + + await log.setup({ + handlers: { + default: handler + }, + loggers: { + default: { + level: "DEBUG", + handlers: ["default"] + } + } + }); + + const logger = log.getLogger(); + + assertEquals(logger.levelName, "DEBUG"); + assertEquals(logger.handlers, [handler]); +}); + +test(async function getLoggerWithName(): Promise<void> { + const fooHandler = new TestHandler("DEBUG"); + + await log.setup({ + handlers: { + foo: fooHandler + }, + loggers: { + bar: { + level: "INFO", + handlers: ["foo"] + } + } + }); + + const logger = log.getLogger("bar"); + + assertEquals(logger.levelName, "INFO"); + assertEquals(logger.handlers, [fooHandler]); +}); + +test(async function getLoggerUnknown(): Promise<void> { + await log.setup({ + handlers: {}, + loggers: {} + }); + + const logger = log.getLogger("nonexistent"); + + assertEquals(logger.levelName, "NOTSET"); + assertEquals(logger.handlers, []); +}); |