Improve logging module (#51)

This commit is contained in:
Bartek Iwańczuk 2019-01-02 15:12:48 +01:00 committed by Ryan Dahl
parent 77831c34b1
commit 439885c756
9 changed files with 278 additions and 172 deletions

View File

@ -26,4 +26,4 @@ jobs:
# steps: # steps:
# - powershell: iex (iwr https://deno.land/x/install/install.ps1) # - powershell: iex (iwr https://deno.land/x/install/install.ps1)
# - script: echo '##vso[task.prependpath]C:\Users\VssAdministrator\.deno\bin\' # - script: echo '##vso[task.prependpath]C:\Users\VssAdministrator\.deno\bin\'
# - script: 'C:\Users\VssAdministrator\.deno\bin\deno.exe test.ts --allow-run --allow-net' # - script: 'C:\Users\VssAdministrator\.deno\bin\deno.exe test.ts --allow-run --allow-net --allow-write'

View File

@ -1,15 +1,38 @@
# Logging module for Deno # Basic usage
Very much work in progress. Contributions welcome. ```ts
import * as log from "https://deno.land/x/std/logging/index.ts";
This library is heavily inspired by Python's // simple console logger
[logging](https://docs.python.org/3/library/logging.html#logging.Logger.log) log.debug("Hello world");
module, altough it's not planned to be a direct port. Having separate loggers, log.info("Hello world");
handlers, formatters and filters gives developer very granular control over log.warning("Hello world");
logging which is most desirable for server side software. log.error("Hello world");
log.critical("500 Internal server error");
Todo: // configure as needed
await log.setup({
handlers: {
console: new log.handlers.ConsoleHandler("DEBUG"),
file: new log.handlers.FileHandler("WARNING", "./log.txt"),
},
- [ ] implement formatters loggers: {
- [ ] implement `FileHandler` default: {
- [ ] tests 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
```

View File

@ -1,18 +0,0 @@
import { getLevelByName } from "./levels.ts";
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) {}
}

65
logging/handlers.ts Normal file
View File

@ -0,0 +1,65 @@
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();
}
}

View File

@ -1,26 +0,0 @@
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;
}
}
}

View File

@ -1,21 +1,14 @@
import { Logger } from "./logger.ts"; import { Logger } from "./logger.ts";
import { BaseHandler } from "./handler.ts"; import { BaseHandler, ConsoleHandler, WriterHandler, FileHandler } from "./handlers.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 { export class LoggerConfig {
level?: string; level?: string;
handlers?: string[]; handlers?: string[];
} }
export interface LoggingConfig { export interface LogConfig {
handlers?: { handlers?: {
[name: string]: HandlerConfig; [name: string]: BaseHandler;
}; };
loggers?: { loggers?: {
[name: string]: LoggerConfig; [name: string]: LoggerConfig;
@ -24,78 +17,95 @@ export interface LoggingConfig {
const DEFAULT_LEVEL = "INFO"; const DEFAULT_LEVEL = "INFO";
const DEFAULT_NAME = ""; const DEFAULT_NAME = "";
const DEFAULT_CONFIG: LoggingConfig = { const DEFAULT_CONFIG: LogConfig = {
handlers: { handlers: {
[DEFAULT_NAME]: {
level: DEFAULT_LEVEL,
class: ConsoleHandler
}
}, },
loggers: { loggers: {
[DEFAULT_NAME]: { "": {
level: DEFAULT_LEVEL, level: "INFO",
handlers: [DEFAULT_NAME] handlers: [""],
} }
} }
}; };
const defaultHandler = new ConsoleHandler("INFO");
const defaultLogger = new Logger("INFO", [defaultHandler]);
const state = { const state = {
defaultHandler,
defaultLogger,
handlers: new Map(),
loggers: new Map(), loggers: new Map(),
config: DEFAULT_CONFIG 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 = { export const handlers = {
BaseHandler: BaseHandler, BaseHandler,
ConsoleHandler: ConsoleHandler 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) { export function getLogger(name?: string) {
if (!name) { if (!name) {
name = DEFAULT_NAME; return defaultLogger;
} }
if (!state.loggers.has(name)) { if (!state.loggers.has(name)) {
return createNewLogger(name); const logger = new Logger("NOTSET", []);
state.loggers.set(name, logger);
return logger;
} }
return state.loggers.get(name); return state.loggers.get(name);
} }
export function setup(config: LoggingConfig) { export async function setup(config: LogConfig) {
state.config = { state.config = config;
handlers: {
...DEFAULT_CONFIG.handlers, // tear down existing handlers
...config.handlers! state.handlers.forEach(handler => {
}, handler.destroy();
loggers: { });
...DEFAULT_CONFIG.loggers, state.handlers.clear();
...config.loggers!
} // 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);

View File

@ -1,4 +1,5 @@
export const LogLevel = { export const LogLevel = {
NOTSET: 0,
DEBUG: 10, DEBUG: 10,
INFO: 20, INFO: 20,
WARNING: 30, WARNING: 30,
@ -7,14 +8,16 @@ export const LogLevel = {
}; };
const byName = { const byName = {
NOTSET: LogLevel.NOTSET,
DEBUG: LogLevel.DEBUG, DEBUG: LogLevel.DEBUG,
INFO: LogLevel.INFO, INFO: LogLevel.INFO,
WARNING: LogLevel.WARNING, WARNING: LogLevel.WARNING,
ERROR: LogLevel.ERROR, ERROR: LogLevel.ERROR,
CRITICAL: LogLevel.DEBUG CRITICAL: LogLevel.CRITICAL
}; };
const byLevel = { const byLevel = {
[LogLevel.NOTSET]: "NOTSET",
[LogLevel.DEBUG]: "DEBUG", [LogLevel.DEBUG]: "DEBUG",
[LogLevel.INFO]: "INFO", [LogLevel.INFO]: "INFO",
[LogLevel.WARNING]: "WARNING", [LogLevel.WARNING]: "WARNING",
@ -22,10 +25,10 @@ const byLevel = {
[LogLevel.CRITICAL]: "CRITICAL" [LogLevel.CRITICAL]: "CRITICAL"
}; };
export function getLevelByName(name) { export function getLevelByName(name: string): number {
return byName[name]; return byName[name];
} }
export function getLevelName(level) { export function getLevelName(level: number): string {
return byLevel[level]; return byLevel[level];
} }

View File

@ -1,44 +1,62 @@
import { LogLevel, getLevelByName, getLevelName } from "./levels.ts"; 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 { export class Logger {
level: number; level: number;
levelName: string; levelName: string;
handlers: any[]; handlers: any[];
constructor(levelName, handlers) { constructor(levelName: string, handlers?: BaseHandler[]) {
this.level = getLevelByName(levelName); this.level = getLevelByName(levelName);
this.levelName = levelName; this.levelName = levelName;
this.handlers = handlers;
this.handlers = handlers || [];
} }
_log(level, ...args) { _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 => { this.handlers.forEach(handler => {
handler.handle(level, ...args); handler.handle(record);
}); });
} }
log(level, ...args) { debug(msg: string, ...args: any[]) {
if (this.level > level) return; return this._log(LogLevel.DEBUG, msg, ...args);
return this._log(level, ...args);
} }
debug(...args) { info(msg: string, ...args: any[]) {
return this.log(LogLevel.DEBUG, ...args); return this._log(LogLevel.INFO, msg, ...args);
} }
info(...args) { warning(msg: string, ...args: any[]) {
return this.log(LogLevel.INFO, ...args); return this._log(LogLevel.WARNING, msg, ...args);
} }
warning(...args) { error(msg: string, ...args: any[]) {
return this.log(LogLevel.WARNING, ...args); return this._log(LogLevel.ERROR, msg, ...args);
} }
error(...args) { critical(msg: string, ...args: any[]) {
return this.log(LogLevel.ERROR, ...args); return this._log(LogLevel.CRITICAL, msg, ...args);
}
critical(...args) {
return this.log(LogLevel.CRITICAL, ...args);
} }
} }

View File

@ -1,53 +1,84 @@
import { remove, open, readAll } from "deno";
import { assertEqual, test } from "https://deno.land/x/testing/testing.ts"; import { assertEqual, test } from "https://deno.land/x/testing/testing.ts";
import * as logging from "index.ts"; import * as log from "index.ts";
import { FileHandler } from "./handlers.ts";
// TODO: establish something more sophisticated // TODO: establish something more sophisticated
let testOutput = ""; let testOutput = "";
class TestHandler extends logging.handlers.BaseHandler { class TestHandler extends log.handlers.BaseHandler {
_log(level, ...args) { constructor(levelName: string) {
testOutput += `${level} ${args[0]}\n`; super(levelName);
}
log(msg: string) {
testOutput += `${msg}\n`;
} }
} }
logging.setup({ test(function testDefaultlogMethods() {
handlers: { log.debug("Foobar");
debug: { log.info("Foobar");
level: "DEBUG", log.warning("Foobar");
class: TestHandler log.error("Foobar");
}, log.critical("Foobar");
info: { const logger = log.getLogger('');
level: "INFO", console.log(logger);
class: TestHandler
}
},
loggers: {
default: {
level: "DEBUG",
handlers: ["debug"]
},
info: {
level: "INFO",
handlers: ["info"]
}
}
}); });
const logger = logging.getLogger("default"); test(async function basicTest() {
const unknownLogger = logging.getLogger("info"); const testFile = './log.txt';
test(function basicTest() { await log.setup({
logger.debug("I should be printed."); handlers: {
unknownLogger.debug("I should not be printed."); debug: new TestHandler("DEBUG"),
unknownLogger.info("And I should be printed as well."); info: new TestHandler("INFO"),
file: new FileHandler("DEBUG", testFile),
},
loggers: {
foo: {
level: "DEBUG",
handlers: ["debug", "file"]
},
bar: {
level: "INFO",
handlers: ["info"]
}
}
});
const fooLogger = log.getLogger("foo");
const barLogger = log.getLogger("bar");
const bazzLogger = log.getLogger("bazz");
fooLogger.debug("I should be logged.");
fooLogger.debug("I should be logged.");
barLogger.debug("I should not be logged.");
barLogger.info("And I should be logged as well.");
bazzLogger.critical("I shouldn't be logged neither.")
const expectedOutput = const expectedOutput =
"10 I should be printed.\n20 And I should be printed as well.\n"; "DEBUG I should be logged.\n" +
"DEBUG I should be logged.\n" +
"INFO And I should be logged as well.\n";
assertEqual(testOutput, expectedOutput); assertEqual(testOutput, expectedOutput);
// same check for file handler
const f = await open(testFile);
const bytes = await readAll(f);
const fileOutput = new TextDecoder().decode(bytes);
await f.close();
await remove(testFile);
const fileExpectedOutput =
"DEBUG I should be logged.\n" +
"DEBUG I should be logged.\n";
assertEqual(fileOutput, fileExpectedOutput);
}); });