Add @rondo.dev/logger, remove winston

This commit is contained in:
Jerko Steiner 2019-08-25 14:08:55 +07:00
parent dffad844ad
commit 9aaf8b62a9
32 changed files with 463 additions and 416 deletions

170
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -0,0 +1,16 @@
module.exports = {
roots: [
'<rootDir>/src'
],
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx'
],
setupFiles: ['<rootDir>/jest.setup.js']
}

View File

@ -0,0 +1,4 @@
if (!process.env.LOG) {
process.env.LOG = 'sql:warn'
}
process.chdir(__dirname)

View File

@ -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"
}

View File

@ -1,4 +1,4 @@
import {ILogger} from './ILogger'
import {ILogger} from './logger/ILogger'
export interface ILoggerFactory {
getLogger(name: string): ILogger

View File

@ -0,0 +1,9 @@
import { LogLevel } from './LogLevel'
export interface IMessage {
loggerName: string
level: LogLevel
timestamp: Date
message: string
params: any[]
}

View File

@ -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))
}

View File

@ -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',
]])
})
})
})

View File

@ -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
}
}

View File

@ -0,0 +1,5 @@
import { IMessage } from '../IMessage'
export interface IFormatter {
format(message: IMessage): IMessage
}

View File

@ -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
}
}

View File

@ -0,0 +1,2 @@
export * from './IFormatter'
export * from './MessageFormatter'

View File

@ -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')
})
})

View File

@ -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 ''
}

View File

@ -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}

View File

@ -0,0 +1,9 @@
type ILogFunction = (message: string, ...meta: any[]) => void
export interface ILogger {
error: ILogFunction
warn: ILogFunction
info: ILogFunction
debug: ILogFunction
verbose: ILogFunction
}

View File

@ -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)
}
}

View File

@ -0,0 +1,2 @@
export * from './ILogger'
export * from './Logger'

View File

@ -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
}
}
}
}

View File

@ -0,0 +1,7 @@
import { IMessage } from '../IMessage'
import { LogLevel } from '../LogLevel'
export interface ITransport {
readonly level: LogLevel
write(message: IMessage): void
}

View File

@ -0,0 +1,2 @@
export * from './ConsoleTransport'
export * from './ITransport'

View File

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "esm"
},
"references": []
}

View File

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.common.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"references": [
]
}

View File

@ -0,0 +1,10 @@
{
"extends": [
"../tslint.json"
],
"linterOptions": {
"exclude": [
"src/migrations/*.ts"
]
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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')
})
})
})

View File

@ -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()

View File

@ -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')

View File

@ -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"}
]
}

View File

@ -6,6 +6,7 @@
},
"references": [
{"path": "../common"},
{"path": "../jsonrpc"}
{"path": "../jsonrpc"},
{"path": "../logger"}
]
}