diff --git a/README.md b/README.md index d3da80550e..1aa6eb5d66 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ for Deno. | Collection | Description | | ---------------------------- | --------------------------------------------------------------- | -| [colors](./colors/README.md) | Modules that generate ANSI color codes for the console. | -| [net](./net/README.md) | A framework for creating HTTP/HTTPS servers inspired by GoLang. | -| [path](./path/README.md) | A path manipulation library. | -| [flags](./flags/README.md) | Command line arguments parser based on minimist. | - +| [colors](./colors/) | Modules that generate ANSI color codes for the console. | +| [net](./net/) | A framework for creating HTTP/HTTPS servers inspired by GoLang. | +| [path](./path/) | File path manipulation. | +| [flags](./flags/) | Command line arguments parser. | +| [logging](./logging/) | Command line logging | --- Copyright 2018 the Deno authors. All rights reserved. MIT license. diff --git a/logging/README.md b/logging/README.md new file mode 100644 index 0000000000..b444b411b3 --- /dev/null +++ b/logging/README.md @@ -0,0 +1,12 @@ +# Logging module for Deno + +Very much work in progress. Contributions welcome. + +This library is heavily inspired by Python's [logging](https://docs.python.org/3/library/logging.html#logging.Logger.log) module, altough +it's not planned to be a direct port. Having separate loggers, handlers, formatters and filters gives developer very granular control over logging +which is most desirable for server side software. + +Todo: +- [ ] implement formatters +- [ ] implement `FileHandler` +- [ ] tests \ No newline at end of file diff --git a/logging/handler.ts b/logging/handler.ts new file mode 100644 index 0000000000..2d76f5a782 --- /dev/null +++ b/logging/handler.ts @@ -0,0 +1,18 @@ +import { getLevelByName } from "./levels"; + +export class BaseHandler { + level: number; + levelName: string; + + constructor(levelName) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + } + + handle(level, ...args) { + if (this.level > level) return; + return this._log(level, ...args); + } + + _log(level, ...args) {} +} diff --git a/logging/handlers/console.ts b/logging/handlers/console.ts new file mode 100644 index 0000000000..219a3baeca --- /dev/null +++ b/logging/handlers/console.ts @@ -0,0 +1,26 @@ +import { BaseHandler } from '../handler.ts'; +import { LogLevel } from '../levels.ts'; + +export class ConsoleHandler extends BaseHandler { + _log(level, ...args) { + switch (level) { + case LogLevel.DEBUG: + console.log(...args); + return; + case LogLevel.INFO: + console.info(...args); + return; + case LogLevel.WARNING: + console.warn(...args); + return; + case LogLevel.ERROR: + console.error(...args); + return; + case LogLevel.CRITICAL: + console.error(...args); + return; + default: + return; + } + } +} diff --git a/logging/index.ts b/logging/index.ts new file mode 100644 index 0000000000..5fabff60f3 --- /dev/null +++ b/logging/index.ts @@ -0,0 +1,101 @@ +import { Logger } from "./logger.ts"; +import { BaseHandler } from "./handler.ts"; +import { ConsoleHandler } from "./handlers/console.ts"; + +export interface HandlerConfig { + // TODO: replace with type describing class derived from BaseHandler + class: typeof BaseHandler; + level?: string; +} + +export class LoggerConfig { + level?: string; + handlers?: string[]; +} + +export interface LoggingConfig { + handlers?: { + [name: string]: HandlerConfig; + }; + loggers?: { + [name: string]: LoggerConfig; + }; +} + +const DEFAULT_LEVEL = "INFO"; +const DEFAULT_NAME = ""; +const DEFAULT_CONFIG: LoggingConfig = { + handlers: { + [DEFAULT_NAME]: { + level: DEFAULT_LEVEL, + class: ConsoleHandler + } + }, + + loggers: { + [DEFAULT_NAME]: { + level: DEFAULT_LEVEL, + handlers: [DEFAULT_NAME] + } + } +}; + +const state = { + loggers: new Map(), + config: DEFAULT_CONFIG +}; + +function createNewHandler(name: string) { + let handlerConfig = state.config.handlers[name]; + + if (!handlerConfig) { + handlerConfig = state.config.handlers[DEFAULT_NAME]; + } + + const constructor = handlerConfig.class; + console.log(constructor); + const handler = new constructor(handlerConfig.level); + return handler; +} + +function createNewLogger(name: string) { + let loggerConfig = state.config.loggers[name]; + + if (!loggerConfig) { + loggerConfig = state.config.loggers[DEFAULT_NAME]; + } + + const handlers = (loggerConfig.handlers || []).map(createNewHandler); + const levelName = loggerConfig.level || DEFAULT_LEVEL; + return new Logger(levelName, handlers); +} + +export const handlers = { + BaseHandler: BaseHandler, + ConsoleHandler: ConsoleHandler +}; + +export function getLogger(name?: string) { + if (!name) { + name = DEFAULT_NAME; + } + + if (!state.loggers.has(name)) { + return createNewLogger(name); + } + + return state.loggers.get(name); +} + +export function setup(config: LoggingConfig) { + state.config = { + handlers: { + ...DEFAULT_CONFIG.handlers, + ...config.handlers! + }, + loggers: { + ...DEFAULT_CONFIG.loggers, + ...config.loggers! + } + }; +} diff --git a/logging/levels.ts b/logging/levels.ts new file mode 100644 index 0000000000..8ba8a8fecc --- /dev/null +++ b/logging/levels.ts @@ -0,0 +1,31 @@ +export const LogLevel = { + DEBUG: 10, + INFO: 20, + WARNING: 30, + ERROR: 40, + CRITICAL: 50 +}; + +const byName = { + DEBUG: LogLevel.DEBUG, + INFO: LogLevel.INFO, + WARNING: LogLevel.WARNING, + ERROR: LogLevel.ERROR, + CRITICAL: LogLevel.DEBUG +}; + +const byLevel = { + [LogLevel.DEBUG]: "DEBUG", + [LogLevel.INFO]: "INFO", + [LogLevel.WARNING]: "WARNING", + [LogLevel.ERROR]: "ERROR", + [LogLevel.CRITICAL]: "CRITICAL" +}; + +export function getLevelByName(name) { + return byName[name]; +} + +export function getLevelName(level) { + return byLevel[level]; +} diff --git a/logging/logger.ts b/logging/logger.ts new file mode 100644 index 0000000000..733b1fd097 --- /dev/null +++ b/logging/logger.ts @@ -0,0 +1,44 @@ +import { LogLevel, getLevelByName, getLevelName } from "./levels.ts"; + +export class Logger { + level: number; + levelName: string; + handlers: any[]; + + constructor(levelName, handlers) { + this.level = getLevelByName(levelName); + this.levelName = levelName; + this.handlers = handlers; + } + + _log(level, ...args) { + this.handlers.forEach(handler => { + handler.handle(level, ...args); + }); + } + + log(level, ...args) { + if (this.level > level) return; + return this._log(level, ...args); + } + + debug(...args) { + return this.log(LogLevel.DEBUG, ...args); + } + + info(...args) { + return this.log(LogLevel.INFO, ...args); + } + + warning(...args) { + return this.log(LogLevel.WARNING, ...args); + } + + error(...args) { + return this.log(LogLevel.ERROR, ...args); + } + + critical(...args) { + return this.log(LogLevel.CRITICAL, ...args); + } +} diff --git a/logging/test.ts b/logging/test.ts new file mode 100644 index 0000000000..365064cbf2 --- /dev/null +++ b/logging/test.ts @@ -0,0 +1,53 @@ +import { assertEqual, test } from "https://deno.land/x/testing/testing.ts"; + +import * as logging from "index.ts"; + +// TODO: establish something more sophisticated + +let testOutput = ""; + +class TestHandler extends logging.handlers.BaseHandler { + _log(level, ...args) { + testOutput += `${level} ${args[0]}\n`; + } +} + +logging.setup({ + handlers: { + debug: { + level: "DEBUG", + class: TestHandler + }, + + info: { + level: "INFO", + class: TestHandler + } + }, + + loggers: { + default: { + level: "DEBUG", + handlers: ["debug"] + }, + + info: { + level: "INFO", + handlers: ["info"] + } + } +}); + +const logger = logging.getLogger("default"); +const unknownLogger = logging.getLogger("info"); + +test(function basicTest() { + logger.debug("I should be printed."); + unknownLogger.debug("I should not be printed."); + unknownLogger.info("And I should be printed as well."); + + const expectedOutput = + "10 I should be printed.\n20 And I should be printed as well.\n"; + + assertEqual(testOutput, expectedOutput); +}); diff --git a/test.ts b/test.ts index f64e2ae950..94534825da 100755 --- a/test.ts +++ b/test.ts @@ -13,6 +13,9 @@ import "net/http_test.ts"; import "net/textproto_test.ts"; import { runTests, completePromise } from "net/file_server_test.ts"; +// logging tests +import "logging/test.ts"; + // file server test const fileServer = run({ args: ["deno", "--allow-net", "net/file_server.ts", "."]