summaryrefslogtreecommitdiff
path: root/std/log
diff options
context:
space:
mode:
Diffstat (limited to 'std/log')
m---------std0
-rw-r--r--std/log/README.md153
-rw-r--r--std/log/handlers.ts120
-rw-r--r--std/log/handlers_test.ts96
-rw-r--r--std/log/levels.ts26
-rw-r--r--std/log/logger.ts65
-rw-r--r--std/log/logger_test.ts93
-rw-r--r--std/log/mod.ts123
-rw-r--r--std/log/test.ts105
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, []);
+});