diff --git a/std/log/README.md b/std/log/README.md index 12a0899422..cc99bbcd3a 100644 --- a/std/log/README.md +++ b/std/log/README.md @@ -19,7 +19,7 @@ await log.setup({ handlers: { console: new log.handlers.ConsoleHandler("DEBUG"), - file: new log.handlers.FileHandler("WARNING", { + file: new log.handlers.FileHandler(log.LogLevels.DEBUG, { filename: "./log.txt", // you can change format of output message using any keys in `LogRecord` formatter: "{levelName} {msg}", @@ -34,7 +34,7 @@ await log.setup({ }, tasks: { - level: "ERROR", + level: log.LogLevels.ERROR, handlers: ["console"], }, }, diff --git a/std/log/handlers.ts b/std/log/handlers.ts index 041f101ed6..b22f458ac2 100644 --- a/std/log/handlers.ts +++ b/std/log/handlers.ts @@ -3,7 +3,7 @@ const { open, openSync, close, renameSync, statSync } = Deno; type File = Deno.File; type Writer = Deno.Writer; type OpenOptions = Deno.OpenOptions; -import { getLevelByName, LogLevel } from "./levels.ts"; +import { getLevelByName, LevelName, LogLevels } from "./levels.ts"; import { LogRecord } from "./logger.ts"; import { red, yellow, blue, bold } from "../fmt/colors.ts"; import { existsSync, exists } from "../fs/exists.ts"; @@ -18,10 +18,10 @@ interface HandlerOptions { export class BaseHandler { level: number; - levelName: string; + levelName: LevelName; formatter: string | FormatterFunction; - constructor(levelName: string, options: HandlerOptions = {}) { + constructor(levelName: LevelName, options: HandlerOptions = {}) { this.level = getLevelByName(levelName); this.levelName = levelName; @@ -62,16 +62,16 @@ export class ConsoleHandler extends BaseHandler { let msg = super.format(logRecord); switch (logRecord.level) { - case LogLevel.INFO: + case LogLevels.INFO: msg = blue(msg); break; - case LogLevel.WARNING: + case LogLevels.WARNING: msg = yellow(msg); break; - case LogLevel.ERROR: + case LogLevels.ERROR: msg = red(msg); break; - case LogLevel.CRITICAL: + case LogLevels.CRITICAL: msg = bold(red(msg)); break; default: @@ -105,7 +105,7 @@ export class FileHandler extends WriterHandler { protected _openOptions: OpenOptions; #encoder = new TextEncoder(); - constructor(levelName: string, options: FileHandlerOptions) { + constructor(levelName: LevelName, options: FileHandlerOptions) { super(levelName, options); this._filename = options.filename; // default to append mode, write only @@ -143,7 +143,7 @@ export class RotatingFileHandler extends FileHandler { #maxBytes: number; #maxBackupCount: number; - constructor(levelName: string, options: RotatingFileHandlerOptions) { + constructor(levelName: LevelName, options: RotatingFileHandlerOptions) { super(levelName, options); this.#maxBytes = options.maxBytes; this.#maxBackupCount = options.maxBackupCount; diff --git a/std/log/handlers_test.ts b/std/log/handlers_test.ts index 13b6de3bd9..561b5c04ac 100644 --- a/std/log/handlers_test.ts +++ b/std/log/handlers_test.ts @@ -1,7 +1,13 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. const { test } = Deno; import { assert, assertEquals, assertThrowsAsync } from "../testing/asserts.ts"; -import { LogLevel, getLevelName, getLevelByName } from "./levels.ts"; +import { + LogLevels, + LogLevelNames, + getLevelName, + getLevelByName, + LevelName, +} from "./levels.ts"; import { BaseHandler, FileHandler, RotatingFileHandler } from "./handlers.ts"; import { LogRecord } from "./logger.ts"; import { existsSync } from "../fs/exists.ts"; @@ -19,7 +25,7 @@ class TestHandler extends BaseHandler { test(function simpleHandler(): void { const cases = new Map([ [ - LogLevel.DEBUG, + LogLevels.DEBUG, [ "DEBUG debug-test", "INFO info-test", @@ -29,7 +35,7 @@ test(function simpleHandler(): void { ], ], [ - LogLevel.INFO, + LogLevels.INFO, [ "INFO info-test", "WARNING warning-test", @@ -38,19 +44,19 @@ test(function simpleHandler(): void { ], ], [ - LogLevel.WARNING, + LogLevels.WARNING, ["WARNING warning-test", "ERROR error-test", "CRITICAL critical-test"], ], - [LogLevel.ERROR, ["ERROR error-test", "CRITICAL critical-test"]], - [LogLevel.CRITICAL, ["CRITICAL critical-test"]], + [LogLevels.ERROR, ["ERROR error-test", "CRITICAL critical-test"]], + [LogLevels.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); + for (const levelName of LogLevelNames) { + const level = getLevelByName(levelName as LevelName); handler.handle( new LogRecord(`${levelName.toLowerCase()}-test`, [], level) ); @@ -67,7 +73,7 @@ test(function testFormatterAsString(): void { formatter: "test {levelName} {msg}", }); - handler.handle(new LogRecord("Hello, world!", [], LogLevel.DEBUG)); + handler.handle(new LogRecord("Hello, world!", [], LogLevels.DEBUG)); assertEquals(handler.messages, ["test DEBUG Hello, world!"]); }); @@ -78,7 +84,7 @@ test(function testFormatterAsFunction(): void { `fn formatter ${logRecord.levelName} ${logRecord.msg}`, }); - handler.handle(new LogRecord("Hello, world!", [], LogLevel.ERROR)); + handler.handle(new LogRecord("Hello, world!", [], LogLevels.ERROR)); assertEquals(handler.messages, ["fn formatter ERROR Hello, world!"]); }); @@ -92,12 +98,12 @@ test({ }); await fileHandler.setup(); - fileHandler.handle(new LogRecord("Hello World", [], LogLevel.WARNING)); + fileHandler.handle(new LogRecord("Hello World", [], LogLevels.WARNING)); await fileHandler.destroy(); const firstFileSize = (await Deno.stat(LOG_FILE)).size; await fileHandler.setup(); - fileHandler.handle(new LogRecord("Hello World", [], LogLevel.WARNING)); + fileHandler.handle(new LogRecord("Hello World", [], LogLevels.WARNING)); await fileHandler.destroy(); const secondFileSize = (await Deno.stat(LOG_FILE)).size; @@ -198,11 +204,11 @@ test({ }); await fileHandler.setup(); - fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR)); // 'ERROR AAA\n' = 10 bytes + fileHandler.handle(new LogRecord("AAA", [], LogLevels.ERROR)); // 'ERROR AAA\n' = 10 bytes assertEquals((await Deno.stat(LOG_FILE)).size, 10); - fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR)); + fileHandler.handle(new LogRecord("AAA", [], LogLevels.ERROR)); assertEquals((await Deno.stat(LOG_FILE)).size, 20); - fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR)); + fileHandler.handle(new LogRecord("AAA", [], LogLevels.ERROR)); // Rollover occurred. Log file now has 1 record, rollover file has the original 2 assertEquals((await Deno.stat(LOG_FILE)).size, 10); assertEquals((await Deno.stat(LOG_FILE + ".1")).size, 20); @@ -238,7 +244,7 @@ test({ mode: "a", }); await fileHandler.setup(); - fileHandler.handle(new LogRecord("AAA", [], LogLevel.ERROR)); // 'ERROR AAA\n' = 10 bytes + fileHandler.handle(new LogRecord("AAA", [], LogLevels.ERROR)); // 'ERROR AAA\n' = 10 bytes await fileHandler.destroy(); const decoder = new TextDecoder(); diff --git a/std/log/levels.ts b/std/log/levels.ts index be960dd572..ed6010ba2d 100644 --- a/std/log/levels.ts +++ b/std/log/levels.ts @@ -1,26 +1,59 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -export const LogLevel: Record = { - 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]; +/** Get log level numeric values through enum constants + */ +export enum LogLevels { + NOTSET = 0, + DEBUG = 10, + INFO = 20, + WARNING = 30, + ERROR = 40, + CRITICAL = 50, } -export function getLevelName(level: number): string { - return byLevel[level]; +/** Permitted log level names */ +export const LogLevelNames = Object.keys(LogLevels).filter((key) => + isNaN(Number(key)) +); + +/** Union of valid log level strings */ +export type LevelName = keyof typeof LogLevels; + +const byLevel: Record = { + [String(LogLevels.NOTSET)]: "NOTSET", + [String(LogLevels.DEBUG)]: "DEBUG", + [String(LogLevels.INFO)]: "INFO", + [String(LogLevels.WARNING)]: "WARNING", + [String(LogLevels.ERROR)]: "ERROR", + [String(LogLevels.CRITICAL)]: "CRITICAL", +}; + +/** Returns the numeric log level associated with the passed, + * stringy log level name. + */ +export function getLevelByName(name: LevelName): number { + switch (name) { + case "NOTSET": + return LogLevels.NOTSET; + case "DEBUG": + return LogLevels.DEBUG; + case "INFO": + return LogLevels.INFO; + case "WARNING": + return LogLevels.WARNING; + case "ERROR": + return LogLevels.ERROR; + case "CRITICAL": + return LogLevels.CRITICAL; + default: + throw new Error(`no log level found for "${name}"`); + } +} + +/** Returns the stringy log level name provided the numeric log level */ +export function getLevelName(level: number): LevelName { + const levelName = byLevel[level]; + if (levelName) { + return levelName; + } + throw new Error(`no level name found for level: ${level}`); } diff --git a/std/log/logger.ts b/std/log/logger.ts index 9653d9008c..6a12325e86 100644 --- a/std/log/logger.ts +++ b/std/log/logger.ts @@ -1,5 +1,10 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -import { LogLevel, getLevelByName, getLevelName } from "./levels.ts"; +import { + LogLevels, + getLevelByName, + getLevelName, + LevelName, +} from "./levels.ts"; import { BaseHandler } from "./handlers.ts"; export class LogRecord { @@ -26,11 +31,11 @@ export class LogRecord { export class Logger { level: number; - levelName: string; + levelName: LevelName; // eslint-disable-next-line @typescript-eslint/no-explicit-any handlers: any[]; - constructor(levelName: string, handlers?: BaseHandler[]) { + constructor(levelName: LevelName, handlers?: BaseHandler[]) { this.level = getLevelByName(levelName); this.levelName = levelName; @@ -48,22 +53,22 @@ export class Logger { } debug(msg: string, ...args: unknown[]): void { - this._log(LogLevel.DEBUG, msg, ...args); + this._log(LogLevels.DEBUG, msg, ...args); } info(msg: string, ...args: unknown[]): void { - this._log(LogLevel.INFO, msg, ...args); + this._log(LogLevels.INFO, msg, ...args); } warning(msg: string, ...args: unknown[]): void { - this._log(LogLevel.WARNING, msg, ...args); + this._log(LogLevels.WARNING, msg, ...args); } error(msg: string, ...args: unknown[]): void { - this._log(LogLevel.ERROR, msg, ...args); + this._log(LogLevels.ERROR, msg, ...args); } critical(msg: string, ...args: unknown[]): void { - this._log(LogLevel.CRITICAL, msg, ...args); + this._log(LogLevels.CRITICAL, msg, ...args); } } diff --git a/std/log/logger_test.ts b/std/log/logger_test.ts index 804591b4f9..1c02dbb1a8 100644 --- a/std/log/logger_test.ts +++ b/std/log/logger_test.ts @@ -2,7 +2,7 @@ const { test } = Deno; import { assertEquals } from "../testing/asserts.ts"; import { LogRecord, Logger } from "./logger.ts"; -import { LogLevel } from "./levels.ts"; +import { LogLevels, LevelName } from "./levels.ts"; import { BaseHandler } from "./handlers.ts"; class TestHandler extends BaseHandler { @@ -23,7 +23,7 @@ test(function simpleLogger(): void { const handler = new TestHandler("DEBUG"); let logger = new Logger("DEBUG"); - assertEquals(logger.level, LogLevel.DEBUG); + assertEquals(logger.level, LogLevels.DEBUG); assertEquals(logger.levelName, "DEBUG"); assertEquals(logger.handlers, []); @@ -41,14 +41,14 @@ test(function customHandler(): void { const record = handler.records[0]; assertEquals(record.msg, "foo"); assertEquals(record.args, [1, 2]); - assertEquals(record.level, LogLevel.DEBUG); + assertEquals(record.level, LogLevels.DEBUG); assertEquals(record.levelName, "DEBUG"); assertEquals(handler.messages, ["DEBUG foo"]); }); test(function logFunctions(): void { - const doLog = (level: string): TestHandler => { + const doLog = (level: LevelName): TestHandler => { const handler = new TestHandler(level); const logger = new Logger(level, [handler]); logger.debug("foo"); diff --git a/std/log/mod.ts b/std/log/mod.ts index 2f068f8ba7..4032937a25 100644 --- a/std/log/mod.ts +++ b/std/log/mod.ts @@ -8,9 +8,12 @@ import { RotatingFileHandler, } from "./handlers.ts"; import { assert } from "../testing/asserts.ts"; +import { LevelName } from "./levels.ts"; + +export { LogLevels } from "./levels.ts"; export class LoggerConfig { - level?: string; + level?: LevelName; handlers?: string[]; } diff --git a/std/log/test.ts b/std/log/test.ts index 858f722e27..47c75ba6ea 100644 --- a/std/log/test.ts +++ b/std/log/test.ts @@ -1,8 +1,13 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. const { test } = Deno; -import { assertEquals } from "../testing/asserts.ts"; +import { assertEquals, assertThrows } from "../testing/asserts.ts"; import * as log from "./mod.ts"; -import { LogLevel } from "./levels.ts"; +import { + LogLevelNames, + LevelName, + getLevelByName, + getLevelName, +} from "./levels.ts"; class TestHandler extends log.handlers.BaseHandler { public messages: string[] = []; @@ -23,13 +28,13 @@ test(async function defaultHandlers(): Promise { CRITICAL: log.critical, }; - for (const levelName in LogLevel) { + for (const levelName of LogLevelNames) { if (levelName === "NOTSET") { continue; } const logger = loggers[levelName]; - const handler = new TestHandler(levelName); + const handler = new TestHandler(levelName as LevelName); await log.setup({ handlers: { @@ -37,7 +42,7 @@ test(async function defaultHandlers(): Promise { }, loggers: { default: { - level: levelName, + level: levelName as LevelName, handlers: ["default"], }, }, @@ -103,3 +108,8 @@ test(async function getLoggerUnknown(): Promise { assertEquals(logger.levelName, "NOTSET"); assertEquals(logger.handlers, []); }); + +test(function getInvalidLoggerLevels(): void { + assertThrows(() => getLevelByName("FAKE_LOG_LEVEL" as LevelName)); + assertThrows(() => getLevelName(5000)); +});