From f626b04ebe320a96a220af29595c6ca84cf9a10a Mon Sep 17 00:00:00 2001 From: Andy Hayden Date: Sat, 12 Jan 2019 13:50:04 -0800 Subject: Reorgnanize repos, examples and tests (denoland/deno_std#105) Original: https://github.com/denoland/deno_std/commit/c5e6e015b5be19027f60c19ca86283d12f9258f3 --- log/README.md | 38 ++++++++++++++++++ log/handlers.ts | 62 +++++++++++++++++++++++++++++ log/levels.ts | 34 ++++++++++++++++ log/logger.ts | 62 +++++++++++++++++++++++++++++ log/mod.ts | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ log/test.ts | 28 +++++++++++++ 6 files changed, 343 insertions(+) create mode 100644 log/README.md create mode 100644 log/handlers.ts create mode 100644 log/levels.ts create mode 100644 log/logger.ts create mode 100644 log/mod.ts create mode 100644 log/test.ts (limited to 'log') diff --git a/log/README.md b/log/README.md new file mode 100644 index 000000000..1d88cb070 --- /dev/null +++ b/log/README.md @@ -0,0 +1,38 @@ +# Basic usage + +```ts +import * as log from "https://deno.land/x/std/logging/index.ts"; + +// simple console logger +log.debug("Hello world"); +log.info("Hello world"); +log.warning("Hello world"); +log.error("Hello world"); +log.critical("500 Internal server error"); + +// configure as needed +await log.setup({ + handlers: { + console: new log.handlers.ConsoleHandler("DEBUG"), + file: new log.handlers.FileHandler("WARNING", "./log.txt") + }, + + loggers: { + default: { + level: "DEBUG", + handlers: ["console", "file"] + } + } +}); + +// get configured logger +const logger = log.getLogger("default"); +logger.debug("fizz"); // <- logs to `console`, because `file` handler requires 'WARNING' level +logger.warning("buzz"); // <- logs to both `console` and `file` handlers + +// 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 +const unknownLogger = log.getLogger("mystery"); +unknownLogger.info("foobar"); // no-op +``` diff --git a/log/handlers.ts b/log/handlers.ts new file mode 100644 index 000000000..a0163e6cd --- /dev/null +++ b/log/handlers.ts @@ -0,0 +1,62 @@ +import { open, File, Writer } from "deno"; +import { getLevelByName } from "./levels.ts"; +import { LogRecord } from "./logger.ts"; + +export class BaseHandler { + level: number; + levelName: string; + + constructor(levelName: string) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + } + + handle(logRecord: LogRecord) { + if (this.level > logRecord.level) return; + + // TODO: implement formatter + const msg = `${logRecord.levelName} ${logRecord.msg}`; + + return this.log(msg); + } + + log(msg: string) {} + async setup() {} + async destroy() {} +} + +export class ConsoleHandler extends BaseHandler { + log(msg: string) { + console.log(msg); + } +} + +export abstract class WriterHandler extends BaseHandler { + protected _writer: Writer; + + log(msg: string) { + const encoder = new TextEncoder(); + // promise is intentionally not awaited + this._writer.write(encoder.encode(msg + "\n")); + } +} + +export class FileHandler extends WriterHandler { + private _file: File; + private _filename: string; + + constructor(levelName: string, filename: string) { + super(levelName); + this._filename = filename; + } + + async setup() { + // open file in append mode - write only + this._file = await open(this._filename, "a"); + this._writer = this._file; + } + + async destroy() { + await this._file.close(); + } +} diff --git a/log/levels.ts b/log/levels.ts new file mode 100644 index 000000000..52d28aea5 --- /dev/null +++ b/log/levels.ts @@ -0,0 +1,34 @@ +export const LogLevel = { + NOTSET: 0, + DEBUG: 10, + INFO: 20, + WARNING: 30, + ERROR: 40, + CRITICAL: 50 +}; + +const byName = { + NOTSET: LogLevel.NOTSET, + DEBUG: LogLevel.DEBUG, + INFO: LogLevel.INFO, + WARNING: LogLevel.WARNING, + ERROR: LogLevel.ERROR, + CRITICAL: LogLevel.CRITICAL +}; + +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 byName[name]; +} + +export function getLevelName(level: number): string { + return byLevel[level]; +} diff --git a/log/logger.ts b/log/logger.ts new file mode 100644 index 000000000..9f34f9c32 --- /dev/null +++ b/log/logger.ts @@ -0,0 +1,62 @@ +import { LogLevel, getLevelByName, getLevelName } from "./levels.ts"; +import { BaseHandler } from "./handlers.ts"; + +export interface LogRecord { + msg: string; + args: any[]; + datetime: Date; + level: number; + levelName: string; +} + +export class Logger { + level: number; + levelName: string; + handlers: any[]; + + constructor(levelName: string, handlers?: BaseHandler[]) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + + this.handlers = handlers || []; + } + + _log(level: number, msg: string, ...args: any[]) { + 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 => { + handler.handle(record); + }); + } + + debug(msg: string, ...args: any[]) { + return this._log(LogLevel.DEBUG, msg, ...args); + } + + info(msg: string, ...args: any[]) { + return this._log(LogLevel.INFO, msg, ...args); + } + + warning(msg: string, ...args: any[]) { + return this._log(LogLevel.WARNING, msg, ...args); + } + + error(msg: string, ...args: any[]) { + return this._log(LogLevel.ERROR, msg, ...args); + } + + critical(msg: string, ...args: any[]) { + return this._log(LogLevel.CRITICAL, msg, ...args); + } +} diff --git a/log/mod.ts b/log/mod.ts new file mode 100644 index 000000000..e8c762ac6 --- /dev/null +++ b/log/mod.ts @@ -0,0 +1,119 @@ +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_NAME = ""; +const DEFAULT_CONFIG: LogConfig = { + handlers: {}, + + loggers: { + "": { + level: "INFO", + handlers: [""] + } + } +}; + +const defaultHandler = new ConsoleHandler("INFO"); +const defaultLogger = new Logger("INFO", [defaultHandler]); + +const state = { + defaultHandler, + defaultLogger, + handlers: new Map(), + loggers: new Map(), + config: DEFAULT_CONFIG +}; + +export const handlers = { + BaseHandler, + ConsoleHandler, + WriterHandler, + FileHandler +}; + +export const debug = (msg: string, ...args: any[]) => + defaultLogger.debug(msg, ...args); +export const info = (msg: string, ...args: any[]) => + defaultLogger.info(msg, ...args); +export const warning = (msg: string, ...args: any[]) => + defaultLogger.warning(msg, ...args); +export const error = (msg: string, ...args: any[]) => + defaultLogger.error(msg, ...args); +export const critical = (msg: string, ...args: any[]) => + defaultLogger.critical(msg, ...args); + +export function getLogger(name?: string) { + if (!name) { + return defaultLogger; + } + + if (!state.loggers.has(name)) { + const logger = new Logger("NOTSET", []); + state.loggers.set(name, logger); + return logger; + } + + return state.loggers.get(name); +} + +export async function setup(config: LogConfig) { + state.config = config; + + // tear down existing handlers + state.handlers.forEach(handler => { + 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 = []; + + handlerNames.forEach(handlerName => { + 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/log/test.ts b/log/test.ts new file mode 100644 index 000000000..fdc994eb7 --- /dev/null +++ b/log/test.ts @@ -0,0 +1,28 @@ +import { remove, open, readAll } from "deno"; +import { assertEqual, test } from "../testing/mod.ts"; +import * as log from "./mod.ts"; +import { FileHandler } from "./handlers.ts"; + +// TODO: establish something more sophisticated +let testOutput = ""; + +class TestHandler extends log.handlers.BaseHandler { + constructor(levelName: string) { + super(levelName); + } + + log(msg: string) { + testOutput += `${msg}\n`; + } +} + +test(function testDefaultlogMethods() { + log.debug("Foobar"); + log.info("Foobar"); + log.warning("Foobar"); + log.error("Foobar"); + log.critical("Foobar"); + + const logger = log.getLogger(""); + console.log(logger); +}); -- cgit v1.2.3