diff --git a/package-lock.json b/package-lock.json index f0d2895..8006ba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2136,6 +2136,9 @@ "@rondo.dev/jsonrpc": { "version": "file:packages/jsonrpc" }, + "@rondo.dev/logger": { + "version": "file:packages/logger" + }, "@rondo.dev/scripts": { "version": "file:packages/scripts" }, @@ -2158,8 +2161,7 @@ "shortid": "^2.2.14", "sqlite3": "^4.0.4", "typeorm": "^0.2.11", - "uuid": "^3.3.2", - "winston": "^3.1.0" + "uuid": "^3.3.2" } }, "@rondo.dev/tasq": { @@ -2985,14 +2987,6 @@ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", "dev": true }, - "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } - }, "async-each": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", @@ -4811,15 +4805,6 @@ "object-visit": "^1.0.0" } }, - "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4833,34 +4818,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "colornames": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", - "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" - }, - "colors": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", - "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" - }, - "colorspace": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.1.tgz", - "integrity": "sha512-pI3btWyiuz7Ken0BWh9Elzsmv2bM9AhA7psXib4anUXy/orfZ/E0MbQwhSOG/9L8hLlalqrU0UhOuqxW1YjmVw==", - "requires": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, "columnify": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", @@ -5784,16 +5741,6 @@ "wrappy": "1" } }, - "diagnostics": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", - "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", - "requires": { - "colorspace": "1.1.x", - "enabled": "1.0.x", - "kuler": "1.0.x" - } - }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -5974,14 +5921,6 @@ "shimmer": "^1.2.0" } }, - "enabled": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", - "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", - "requires": { - "env-variable": "0.0.x" - } - }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -6016,11 +5955,6 @@ "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", "dev": true }, - "env-variable": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", - "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" - }, "err-code": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", @@ -6617,11 +6551,6 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fast-safe-stringify": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", - "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" - }, "fb-watchman": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", @@ -6646,11 +6575,6 @@ "ua-parser-js": "^0.7.18" } }, - "fecha": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", - "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" - }, "figgy-pudding": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", @@ -9752,14 +9676,6 @@ "integrity": "sha512-3h7B2WRT5LNXOtQiAaWonilegHcPSf9nLVXlSTci8lu1dZUuui61+EsPEZqSVxY7rXYmB2DVKMQILxaO5WL61Q==", "dev": true }, - "kuler": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", - "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", - "requires": { - "colornames": "^1.1.1" - } - }, "labeled-stream-splicer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.1.tgz", @@ -9867,7 +9783,8 @@ "lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true }, "lodash._reinterpolate": { "version": "3.0.0", @@ -9972,18 +9889,6 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, - "logform": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-1.10.0.tgz", - "integrity": "sha512-em5ojIhU18fIMOw/333mD+ZLE2fis0EzXl1ZwHx4iQzmpQi6odNiY/t+ITNr33JZhT9/KEaH+UPIipr6a9EjWg==", - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.2.0" - } - }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10444,7 +10349,8 @@ "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true }, "multimatch": { "version": "3.0.0", @@ -11319,11 +11225,6 @@ "wrappy": "1" } }, - "one-time": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", - "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" - }, "onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", @@ -13383,21 +13284,6 @@ "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", "dev": true }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, "sisteransi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.0.tgz", @@ -13769,11 +13655,6 @@ "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, "stack-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", @@ -14206,11 +14087,6 @@ "integrity": "sha512-F91ZqLgvi1E0PdvmxMgp+gcf6q8fMH7mhdwWfzXnl1k+GbpQDmi8l7DzLC5JTASKbwpY3TfxajAUzAXcv2NmsQ==", "dev": true }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, "thenify": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", @@ -14463,11 +14339,6 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, "true-case-path": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", @@ -15586,31 +15457,6 @@ "execa": "^1.0.0" } }, - "winston": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.1.0.tgz", - "integrity": "sha512-FsQfEE+8YIEeuZEYhHDk5cILo1HOcWkGwvoidLrDgPog0r4bser1lEIOco2dN9zpDJ1M88hfDgZvxe5z4xNcwg==", - "requires": { - "async": "^2.6.0", - "diagnostics": "^1.1.1", - "is-stream": "^1.1.0", - "logform": "^1.9.1", - "one-time": "0.0.4", - "readable-stream": "^2.3.6", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.2.0" - } - }, - "winston-transport": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", - "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", - "requires": { - "readable-stream": "^2.3.6", - "triple-beam": "^1.2.0" - } - }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", diff --git a/package.json b/package.json index 335c139..20d8735 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "@rondo.dev/tasq": "file:packages/tasq", "@rondo.dev/jsonrpc": "file:packages/jsonrpc", "@rondo.dev/scripts": "file:packages/scripts", - "@rondo.dev/argparse": "file:packages/argparse" + "@rondo.dev/argparse": "file:packages/argparse", + "@rondo.dev/logger": "file:packages/logger" }, "devDependencies": { "@types/bcrypt": "^3.0.0", diff --git a/packages/logger/jest.config.js b/packages/logger/jest.config.js new file mode 100644 index 0000000..737dab0 --- /dev/null +++ b/packages/logger/jest.config.js @@ -0,0 +1,16 @@ +module.exports = { + roots: [ + '/src' + ], + transform: { + '^.+\\.tsx?$': 'ts-jest' + }, + testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$', + moduleFileExtensions: [ + 'ts', + 'tsx', + 'js', + 'jsx' + ], + setupFiles: ['/jest.setup.js'] +} diff --git a/packages/logger/jest.setup.js b/packages/logger/jest.setup.js new file mode 100644 index 0000000..a952c9b --- /dev/null +++ b/packages/logger/jest.setup.js @@ -0,0 +1,4 @@ +if (!process.env.LOG) { + process.env.LOG = 'sql:warn' +} +process.chdir(__dirname) diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 0000000..2aaf659 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,15 @@ +{ + "name": "@rondo.dev/logger", + "private": true, + "scripts": { + "test": "jest", + "lint": "tslint --project .", + "compile": "tsc", + "clean": "rm -rf lib/" + }, + "dependencies": {}, + "main": "lib/index.js", + "module": "esm/index.js", + "types": "lib/index.d.ts", + "module": "esm/index.js" +} diff --git a/packages/server/src/logger/ILoggerFactory.ts b/packages/logger/src/ILoggerFactory.ts similarity index 63% rename from packages/server/src/logger/ILoggerFactory.ts rename to packages/logger/src/ILoggerFactory.ts index ede3487..d02f43e 100644 --- a/packages/server/src/logger/ILoggerFactory.ts +++ b/packages/logger/src/ILoggerFactory.ts @@ -1,4 +1,4 @@ -import {ILogger} from './ILogger' +import {ILogger} from './logger/ILogger' export interface ILoggerFactory { getLogger(name: string): ILogger diff --git a/packages/logger/src/IMessage.ts b/packages/logger/src/IMessage.ts new file mode 100644 index 0000000..8fdeb06 --- /dev/null +++ b/packages/logger/src/IMessage.ts @@ -0,0 +1,9 @@ +import { LogLevel } from './LogLevel' + +export interface IMessage { + loggerName: string + level: LogLevel + timestamp: Date + message: string + params: any[] +} diff --git a/packages/logger/src/LogLevel.ts b/packages/logger/src/LogLevel.ts new file mode 100644 index 0000000..ac0b3e9 --- /dev/null +++ b/packages/logger/src/LogLevel.ts @@ -0,0 +1,12 @@ +export enum LogLevel { + OFF, + ERROR, + WARN, + INFO, + DEBUG, + VERBOSE, +} + +export function isLogLevel(value: string): value is keyof typeof LogLevel { + return LogLevel.hasOwnProperty(value) && isNaN(Number(value)) +} diff --git a/packages/logger/src/LoggerFactory.test.ts b/packages/logger/src/LoggerFactory.test.ts new file mode 100644 index 0000000..8d29a5e --- /dev/null +++ b/packages/logger/src/LoggerFactory.test.ts @@ -0,0 +1,89 @@ +import stdMocks from 'std-mocks' +import loggerFactory, { LoggerFactory } from './' + +describe('LoggerFactory', () => { + + let getLogger: typeof LoggerFactory.prototype.getLogger + beforeEach(() => { + getLogger = LoggerFactory.createFromEnv({ + logs: 'test1:verbose,-test3,t4,logtest5', + }) + .getLogger + + stdMocks.use() + + global.console.log = jest.fn() + global.console.warn = jest.fn() + global.console.info = jest.fn() + global.console.debug = jest.fn() + global.console.error = jest.fn() + }) + + afterEach(() => { + stdMocks.flush() + stdMocks.restore() + }) + + it('logs when enabled', () => { + const l1 = getLogger('test1') + const l2 = getLogger('test1') + const l3 = getLogger('test3') + const l4 = getLogger('t4') + const l5 = getLogger('logtest5') + const l6 = getLogger('logtest6') + expect(l1).toBe(l2) + expect(l2).not.toBe(l3) + l2.debug('test A') + l3.debug('test B') + l3.info('test C') + l4.info('test D') + l5.info('test E', { test: 5 }) + l6.info('test') + }) + + it('logs when enabled', () => { + const l1 = getLogger('test1') + l1.verbose('output: %d', 1) + l1.debug('output: %d', 2) + l1.warn('output: %d', 3) + l1.info('output: %d', 4) + l1.error('output: %d', 5) + + expect((global.console.warn as any).mock.calls).toEqual([[ + 'test1 WARN output: 3', + ]]) + expect((global.console.debug as any).mock.calls).toEqual([[ + 'test1 VERBO output: 1', + ], [ + 'test1 DEBUG output: 2', + ]]) + expect((global.console.log as any).mock.calls).toEqual([[ + 'test1 INFO output: 4', + ]]) + expect((global.console.error as any).mock.calls).toEqual([[ + 'test1 ERROR output: 5', + ]]) + }) + + describe('getCorrelationId', () => { + it('returns an empty string by default', () => { + expect(loggerFactory.getCorrelationId()).toBe('') + }) + }) + + describe('create', () => { + it('creates a logger with defaults', () => { + LoggerFactory.createFromEnv() + }) + it('logs all', () => { + const l = LoggerFactory.createFromEnv({ logs: '*' }).getLogger('test') + l.info('test info') + l.debug('test debug') + expect((global.console.debug as any).mock.calls).toEqual([]) + expect((global.console.log as any).mock.calls).toEqual([[ + 'test INFO test info', + ]]) + }) + }) + +}) diff --git a/packages/logger/src/LoggerFactory.ts b/packages/logger/src/LoggerFactory.ts new file mode 100644 index 0000000..4abce9f --- /dev/null +++ b/packages/logger/src/LoggerFactory.ts @@ -0,0 +1,66 @@ +import { MessageFormatter } from './formatters' +import { getDefaultParams } from './getDefaultParams' +import { ILoggerFactory } from './ILoggerFactory' +import { ILogger, Logger } from './logger' +import { isLogLevel, LogLevel } from './LogLevel' +import { ConsoleTransport } from './transports' + +// logging can be configured via environment variables, for example: +// `LOG='*:info,api:debug,-sql' node ...` sets all logs to info, api to debug, +// and disable all sql logs. + +export interface IEnabledLoggers { + readonly [key: string]: LogLevel +} + +export interface ILoggerOptions { + readonly enabledLoggers: IEnabledLoggers +} + +export class LoggerFactory implements ILoggerFactory { + + protected readonly defaultLogLevel: LogLevel + protected readonly loggers: {[key: string]: ILogger} = {} + getCorrelationId: () => string = () => '' + + static createFromEnv({ + logs = getDefaultParams(), + } = {}) { + const enabledLoggers = logs.split(',').reduce((logConfig, log) => { + const [key, value] = log.split(':') + const level = value && value.toUpperCase() + logConfig[key] = isLogLevel(level) ? LogLevel[level] : LogLevel.INFO + return logConfig + }, {} as {[key: string]: LogLevel}) + + return new this({enabledLoggers}) + } + + constructor(readonly options: ILoggerOptions) { + this.defaultLogLevel = options.enabledLoggers['*'] || LogLevel.OFF + } + + getLoggerLevel(name: string): LogLevel { + const {enabledLoggers} = this.options + const disabled = !!enabledLoggers['-' + name] + if (disabled) { + return LogLevel.OFF + } + return enabledLoggers[name] || this.defaultLogLevel + } + + getLogger = (name: string): ILogger => { + if (this.loggers[name]) { + return this.loggers[name] + } + + const level = this.getLoggerLevel(name) + const logger = this.loggers[name] = new Logger({ + name, + formatters: [new MessageFormatter()], + transports: [new ConsoleTransport(level)], + }) + + return logger + } +} diff --git a/packages/logger/src/formatters/IFormatter.ts b/packages/logger/src/formatters/IFormatter.ts new file mode 100644 index 0000000..25e34f4 --- /dev/null +++ b/packages/logger/src/formatters/IFormatter.ts @@ -0,0 +1,5 @@ +import { IMessage } from '../IMessage' + +export interface IFormatter { + format(message: IMessage): IMessage +} diff --git a/packages/logger/src/formatters/MessageFormatter.ts b/packages/logger/src/formatters/MessageFormatter.ts new file mode 100644 index 0000000..8492fd8 --- /dev/null +++ b/packages/logger/src/formatters/MessageFormatter.ts @@ -0,0 +1,26 @@ +import { IFormatter } from './IFormatter' +import { IMessage } from '../IMessage' +import { LogLevel } from '../LogLevel' +import { format } from 'util' + +function padleft(str: string, len: number) { + if (str.length > len) { + return str.substring(0, len) + } + while (str.length < len) { + str += ' ' + } + return str +} + +export class MessageFormatter implements IFormatter { + format(message: IMessage) { + message.message = format( + '%s %s %s', + message.loggerName, + padleft(LogLevel[message.level], 5), + format(message.message, ...message.params), + ) + return message + } +} diff --git a/packages/logger/src/formatters/index.ts b/packages/logger/src/formatters/index.ts new file mode 100644 index 0000000..a06ed59 --- /dev/null +++ b/packages/logger/src/formatters/index.ts @@ -0,0 +1,2 @@ +export * from './IFormatter' +export * from './MessageFormatter' diff --git a/packages/logger/src/getDefaultParams.test.ts b/packages/logger/src/getDefaultParams.test.ts new file mode 100644 index 0000000..7619fc8 --- /dev/null +++ b/packages/logger/src/getDefaultParams.test.ts @@ -0,0 +1,36 @@ +import { getDefaultParams } from './getDefaultParams' + +describe('getDefaultParams', () => { + + const window = (global as any).window + beforeEach(() => { + delete process.env.LOG + }) + + afterEach(() => { + (global as any).window = window + }) + + it('returns an empty string', () => { + delete (global as any).window + expect(getDefaultParams()).toEqual('') + }) + + it('reads process.env.LOG if available', () => { + process.env.LOG = 'test-env' + expect(getDefaultParams()).toEqual('test-env') + }) + + it('reads from localStorage.LOG if available', () => { + localStorage.setItem('LOG', 'test-ls') + expect(getDefaultParams()).toEqual('test-ls') + localStorage.removeItem('LOG') + expect(getDefaultParams()).toEqual('') + }) + + it('reads from localStorage.LOG if available', () => { + localStorage.setItem('LOG', 'test-ls') + expect(getDefaultParams()).toEqual('test-ls') + }) + +}) diff --git a/packages/logger/src/getDefaultParams.ts b/packages/logger/src/getDefaultParams.ts new file mode 100644 index 0000000..3d97ccb --- /dev/null +++ b/packages/logger/src/getDefaultParams.ts @@ -0,0 +1,15 @@ +export function getDefaultParams(): string { + if (typeof process !== 'undefined' && + typeof process.env !== 'undefined' && + typeof process.env.LOG !== 'undefined') { + return process.env.LOG + } + if ( + typeof window !== 'undefined' && + window.localStorage && + typeof window.localStorage.getItem === 'function' + ) { + return window.localStorage.getItem('LOG') || '' + } + return '' +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 0000000..271cea4 --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,17 @@ +export * from './ILoggerFactory' +export * from './IMessage' +export * from './LogLevel' +export * from './Logger' +export * from './LoggerFactory' + +import {LoggerFactory} from './LoggerFactory' +export default LoggerFactory.createFromEnv() + +import * as transports from './transports' +export {transports} + +import * as formatters from './formatters' +export {formatters} + +import * as logger from './logger' +export {logger} diff --git a/packages/logger/src/logger/ILogger.ts b/packages/logger/src/logger/ILogger.ts new file mode 100644 index 0000000..132b1a6 --- /dev/null +++ b/packages/logger/src/logger/ILogger.ts @@ -0,0 +1,9 @@ +type ILogFunction = (message: string, ...meta: any[]) => void + +export interface ILogger { + error: ILogFunction + warn: ILogFunction + info: ILogFunction + debug: ILogFunction + verbose: ILogFunction +} diff --git a/packages/logger/src/logger/Logger.ts b/packages/logger/src/logger/Logger.ts new file mode 100644 index 0000000..85656ef --- /dev/null +++ b/packages/logger/src/logger/Logger.ts @@ -0,0 +1,51 @@ +import { IFormatter } from '../formatters' +import { IMessage } from '../IMessage' +import { LogLevel } from '../LogLevel' +import { ITransport } from '../transports' +import { ILogger } from './ILogger' + +interface ILoggerParams { + name: string + readonly formatters: readonly IFormatter[], + readonly transports: readonly ITransport[], +} + +export class Logger implements ILogger { + constructor(protected readonly config: ILoggerParams) {} + + protected log(level: LogLevel, message: string, params: any[]) { + const initialMessage: IMessage = { + loggerName: this.config.name, + timestamp: new Date(), + message, + params, + level, + } + const formattedMessage = this.config.formatters.reduce((m, f) => { + return f.format(m) + }, initialMessage) + + this.config.transports.forEach(t => { + if (formattedMessage.level <= t.level) { + t.write(formattedMessage) + } + }) + } + + error(message: string, ...args: any[]) { + this.log(LogLevel.ERROR, message, args) + } + warn(message: string, ...args: any[]) { + this.log(LogLevel.WARN, message, args) + + } + info(message: string, ...args: any[]) { + this.log(LogLevel.INFO, message, args) + } + debug(message: string, ...args: any[]) { + this.log(LogLevel.DEBUG, message, args) + } + verbose(message: string, ...args: any[]) { + this.log(LogLevel.VERBOSE, message, args) + } +} diff --git a/packages/logger/src/logger/index.ts b/packages/logger/src/logger/index.ts new file mode 100644 index 0000000..625f6ac --- /dev/null +++ b/packages/logger/src/logger/index.ts @@ -0,0 +1,2 @@ +export * from './ILogger' +export * from './Logger' diff --git a/packages/logger/src/transports/ConsoleTransport.ts b/packages/logger/src/transports/ConsoleTransport.ts new file mode 100644 index 0000000..18a6f0b --- /dev/null +++ b/packages/logger/src/transports/ConsoleTransport.ts @@ -0,0 +1,32 @@ +import { ITransport } from './ITransport' +import { IMessage } from '../IMessage' +import { LogLevel } from '../LogLevel' + +export class ConsoleTransport implements ITransport { + constructor(readonly level: LogLevel) {} + + write(entry: IMessage) { + if (entry.level <= this.level) { + switch (entry.level) { + case LogLevel.ERROR: + // tslint:disable-next-line + console.error(entry.message) + break + case LogLevel.INFO: + // tslint:disable-next-line + console.log(entry.message) + break + case LogLevel.WARN: + // tslint:disable-next-line + console.warn(entry.message) + break + case LogLevel.VERBOSE: + case LogLevel.DEBUG: + // tslint:disable-next-line + console.debug(entry.message) + case LogLevel.OFF: + // do nothing + } + } + } +} diff --git a/packages/logger/src/transports/ITransport.ts b/packages/logger/src/transports/ITransport.ts new file mode 100644 index 0000000..6601835 --- /dev/null +++ b/packages/logger/src/transports/ITransport.ts @@ -0,0 +1,7 @@ +import { IMessage } from '../IMessage' +import { LogLevel } from '../LogLevel' + +export interface ITransport { + readonly level: LogLevel + write(message: IMessage): void +} diff --git a/packages/logger/src/transports/index.ts b/packages/logger/src/transports/index.ts new file mode 100644 index 0000000..714cc7e --- /dev/null +++ b/packages/logger/src/transports/index.ts @@ -0,0 +1,2 @@ +export * from './ConsoleTransport' +export * from './ITransport' diff --git a/packages/logger/tsconfig.esm.json b/packages/logger/tsconfig.esm.json new file mode 100644 index 0000000..915284d --- /dev/null +++ b/packages/logger/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "esm" + }, + "references": [] +} diff --git a/packages/logger/tsconfig.json b/packages/logger/tsconfig.json new file mode 100644 index 0000000..94e864b --- /dev/null +++ b/packages/logger/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.common.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "references": [ + ] +} diff --git a/packages/logger/tslint.json b/packages/logger/tslint.json new file mode 100644 index 0000000..1a7ba23 --- /dev/null +++ b/packages/logger/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": [ + "../tslint.json" + ], + "linterOptions": { + "exclude": [ + "src/migrations/*.ts" + ] + } +} diff --git a/packages/server/package.json b/packages/server/package.json index b5fa967..075e3b9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -30,10 +30,9 @@ "shortid": "^2.2.14", "sqlite3": "^4.0.4", "typeorm": "^0.2.11", - "uuid": "^3.3.2", - "winston": "^3.1.0" + "uuid": "^3.3.2" }, "main": "lib/index.js", "types": "lib/index.d.ts", "module": "esm/index.js" -} \ No newline at end of file +} diff --git a/packages/server/src/application/Application.ts b/packages/server/src/application/Application.ts index 95dae2d..9434467 100644 --- a/packages/server/src/application/Application.ts +++ b/packages/server/src/application/Application.ts @@ -14,7 +14,8 @@ import {ILogger} from '../logger/ILogger' import {IRoutes} from '@rondo.dev/common' import {IServices} from './IServices' import {ITransactionManager} from '../database/ITransactionManager' -import {loggerFactory, LoggerFactory} from '../logger/LoggerFactory' +import {loggerFactory} from '../logger' +import {ILoggerFactory} from '@rondo.dev/logger' import {json} from 'body-parser' export class Application implements IApplication { @@ -25,7 +26,7 @@ export class Application implements IApplication { readonly services: IServices readonly authenticator: middleware.Authenticator - readonly loggerFactory: LoggerFactory = loggerFactory + readonly loggerFactory: ILoggerFactory = loggerFactory constructor(readonly config: IConfig, readonly database: IDatabase) { this.transactionManager = database.transactionManager diff --git a/packages/server/src/logger/LoggerFactory.test.ts b/packages/server/src/logger/LoggerFactory.test.ts deleted file mode 100644 index fb1070d..0000000 --- a/packages/server/src/logger/LoggerFactory.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import {LoggerFactory, pad} from './LoggerFactory' -import stdMocks from 'std-mocks' - -describe('LoggerFactory', () => { - - let getLogger: typeof LoggerFactory.prototype.getLogger - beforeEach(() => { - getLogger = LoggerFactory.createFromEnv({ - logs: 'test1:debug,-test3,t4,logtest5', - opts: '', - }) - .getLogger - - stdMocks.use(); - // since winston uses console._stdout - (console as any)._stdout.write = process.stdout.write; - (console as any)._stderr.write = process.stderr.write - }) - - afterEach(() => { - stdMocks.restore(); - (console as any)._stdout.write = process.stdout.write; - (console as any)._stderr.write = process.stderr.write - }) - - it('logs when enabled', () => { - const l1 = getLogger('test1') - const l2 = getLogger('test1') - const l3 = getLogger('test3') - const l4 = getLogger('t4') - const l5 = getLogger('logtest5') - const l6 = getLogger('logtest6') - expect(l1).toBe(l2) - expect(l2).not.toBe(l3) - l2.debug('test A') - l3.debug('test B') - l3.info('test C') - l4.info('test D') - l5.info('test E', { test: 5 }) - l6.info('test') - - // const output = stdMocks.flush() - // expect(output.stderr.length).toBe(0) - // expect(output.stdout.length).toBe(3) - // expect(output.stdout[0]).toMatch(/debug test1 test A\n$/) - // expect(output.stdout[1]).toMatch(/info {2}t4 {4}test D\n$/) - // expect(output.stdout[2]).toMatch(/info {2}logte test E\n$/) - }) - - describe('opts', () => { - const cases: {[key: string]: RegExp} = { - logstash: /"@message":"test"/, - color: /debug test1 test/, - json: /"message":"test"/, - } - - Object.keys(cases).forEach(opt => { - describe(opt, () => { - it(`logs in ${opt} format`, () => { - getLogger = LoggerFactory.createFromEnv({ - opts: opt, - logs: 'test1:debug', - }).getLogger - const l1 = getLogger('test1') - l1.debug('test') - // const output = stdMocks.flush() - // expect(output.stderr.length).toBe(0) - // expect(output.stdout.length).toBe(1) - // expect(output.stdout[0]).toMatch(cases[opt]) - }) - }) - }) - - }) - - describe('create', () => { - it('creates a logger with defaults', () => { - LoggerFactory.createFromEnv() - }) - it('logs all', () => { - const l = LoggerFactory.createFromEnv({ logs: '*' }).getLogger('test') - l.info('test123') - // const output = stdMocks.flush() - // expect(output.stderr.length).toBe(0) - // expect(output.stdout.length).toBe(1) - // expect(output.stdout[0]).toMatch(/info {2}test {2}test123\n$/) - }) - }) - - describe('pad', () => { - it('pads space to right', () => { - expect(pad('test', 10, false)).toBe('test ') - expect(pad('test', 10, true)).toBe('test ') - }) - it('does nothing when padding or trmming not needed', () => { - expect(pad('test', 4, false)).toBe('test') - expect(pad('test', 4, true)).toBe('test') - }) - it('trims when trimming enabled and needed', () => { - expect(pad('test', 3, false)).toBe('test') - expect(pad('test', 3, true)).toBe('tes') - }) - }) - -}) diff --git a/packages/server/src/logger/LoggerFactory.ts b/packages/server/src/logger/LoggerFactory.ts deleted file mode 100644 index 3d91dea..0000000 --- a/packages/server/src/logger/LoggerFactory.ts +++ /dev/null @@ -1,138 +0,0 @@ -import {ILogger} from './ILogger' -import {ILoggerFactory} from './ILoggerFactory' -import {createLogger, format, transports} from 'winston' - -// logging can be configured via environment variables, for example: -// `LOG='*:info,api:debug,-sql' node ...` sets all logs to info, api to debug, -// and disable all sql logs. - -export function pad(text: string, n: number, trim: boolean) { - text = String(text) - if (text.length >= n) { - return trim ? text.substring(0, n) : text - } - while (text.length < n) { - text += ' ' - } - return text -} - -export type TLogLevel = 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'off' - -export interface IEnabledLoggers { - readonly [key: string]: TLogLevel -} - -export interface IParams { - readonly json: boolean - readonly color: boolean - readonly logstash: boolean, -} - -export interface ILoggerOptions { - readonly enabledLoggers: IEnabledLoggers - readonly levelPad: number - readonly namePad: number - readonly params: IParams -} - -export class LoggerFactory implements ILoggerFactory { - - protected readonly defaultLogLevel: string - protected readonly loggers: {[key: string]: ILogger} = {} - getCorrelationId: () => string - - static createFromEnv({ - logs = process.env.LOG || '', - opts = process.env.LOG_OPTS || '', - } = {}) { - const enabledLoggers = logs.split(',').reduce((logConfig, log) => { - const [key, value] = log.split(':') - logConfig[key] = (value || 'info') as TLogLevel - return logConfig - }, {} as {[key: string]: TLogLevel}) - - const params = opts.split(',').reduce((o, key) => { - o[key] = true - return o - }, {} as {[key: string]: boolean}) - - return new this({ - enabledLoggers, - levelPad: 5, - namePad: 5, - params: params as any as IParams, - }) - } - - constructor(readonly options: ILoggerOptions) { - this.defaultLogLevel = options.enabledLoggers['*'] || 'off' - this.getCorrelationId = () => '' - } - - getLoggerLevel(name: string): TLogLevel { - const {enabledLoggers} = this.options - const disabled = !!enabledLoggers['-' + name] - if (disabled) { - return 'off' - } - return enabledLoggers[name] || this.defaultLogLevel - } - - getLogger = (name: string): ILogger => { - if (this.loggers[name]) { - return this.loggers[name] - } - const {levelPad, namePad, params} = this.options - - const addName = format((info, opts) => { - info.name = name - return info - }) - - const prettyFormat = format(info => { - info.message = info.timestamp + ' ' + - pad(info.level, levelPad, true) + ' ' + - pad(name, namePad, true) + ' ' + - this.getCorrelationId() + ' ' + - info.message - return info - }) - - const print = format.printf(info => { - return info.message - }) - - const formatters = [ - addName(), - format.timestamp(), - format.splat(), - ].filter(f => !!f) - - if (params.logstash) { - formatters.push(format.logstash()) - } else if (params.json) { - formatters.push(format.json()) - } else if (params.color) { - formatters.push(prettyFormat()) - formatters.push(format.colorize({ all: true })) - formatters.push(print) - } else { - formatters.push(prettyFormat()) - formatters.push(print) - } - - const logger = this.loggers[name] = createLogger({ - format: format.combine.apply(format, formatters), - transports: [ - new transports.Console({ - handleExceptions: false, - level: this.getLoggerLevel(name), - }), - ], - }) - return logger - } -} - -export const loggerFactory = LoggerFactory.createFromEnv() diff --git a/packages/server/src/logger/index.ts b/packages/server/src/logger/index.ts index 8d9ed35..ea3db8d 100644 --- a/packages/server/src/logger/index.ts +++ b/packages/server/src/logger/index.ts @@ -1,6 +1,6 @@ -export * from './LoggerFactory' export * from './SQLLogger' -import {loggerFactory} from './LoggerFactory' +import loggerFactory from '@rondo.dev/logger' +export {loggerFactory} export const getLogger = loggerFactory.getLogger export const apiLogger = getLogger('api') diff --git a/packages/server/tsconfig.esm.json b/packages/server/tsconfig.esm.json index 471e554..d05f413 100644 --- a/packages/server/tsconfig.esm.json +++ b/packages/server/tsconfig.esm.json @@ -5,6 +5,7 @@ }, "references": [ {"path": "../common/tsconfig.esm.json"}, - {"path": "../jsonrpc/tsconfig.esm.json"} + {"path": "../jsonrpc/tsconfig.esm.json"}, + {"path": "../logger/tsconfig.esm.json"} ] } diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index 83869b7..900a17f 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -6,6 +6,7 @@ }, "references": [ {"path": "../common"}, - {"path": "../jsonrpc"} + {"path": "../jsonrpc"}, + {"path": "../logger"} ] }