Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: destructured constructor #22

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codecov.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
registry-url: https://registry.npmjs.org/
- run: npm install
- run: npm run test-coverage
- run: npm run test
- uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 2 additions & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
registry-url: https://registry.npmjs.org/
Expand All @@ -18,3 +18,4 @@ jobs:
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NODE_AUTH_TOKEN}}
- uses: coverallsapp/github-action@master
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
lts/*
20
5 changes: 3 additions & 2 deletions benchmark.ts
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env -S npx ts-node
import { performance } from 'perf_hooks'
import Logger, { LogLevel } from './src'
import { Logger, LogLevel } from './src'

// test runner
function run(name: string, fn: () => any, count: number = 1000) {
Expand All @@ -24,7 +25,7 @@ function run(name: string, fn: () => any, count: number = 1000) {

// setup
const NullWriter = () => null
const logger = new Logger(LogLevel.INFO, NullWriter)
const logger = new Logger({ level: LogLevel.INFO, write: NullWriter })

// run tests
;[
Expand Down
19 changes: 0 additions & 19 deletions jest.config.js

This file was deleted.

6,951 changes: 0 additions & 6,951 deletions package-lock.json

This file was deleted.

15 changes: 7 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dexlog",
"version": "1.1.1",
"version": "2.0.0",
"description": "a logger for developers",
"homepage": "https://github.com/pwmcintyre/dexlog",
"author": "Peter McIntyre",
Expand All @@ -24,19 +24,18 @@
"scripts": {
"clean": "rm -rf dist coverage",
"lint": "prettier --write .",
"test": "jest",
"test-coverage": "jest --coverage",
"test": "c8 --reporter=lcov --reporter=text node --require ts-node/register --test ./src/**/*.test.ts ./src/**/**/*.test.ts",
"test:watch": "node --require ts-node/register --test --watch ./src/**/*.test.ts ./src/**/**/*.test.ts",
"prebuild": "npm run clean && npm run lint && npm run test",
"build": "tsc",
"benchmark": "ts-node ./benchmark.ts",
"prepare": "npm run build"
},
"devDependencies": {
"@types/jest": "^26.0.1",
"jest": "^26.1.0",
"@types/node": "^20.2.1",
"c8": "^7.13.0",
"prettier": "2.0.5",
"ts-jest": "^26.1.1",
"ts-node": "^8.10.2",
"typescript": "^3.8.3"
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
}
}
98 changes: 76 additions & 22 deletions src/Logger/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { Logger, LogLevel, Serializer, Stamper, Writer } from '..'
import assert from 'node:assert'
import { describe, test } from 'node:test'
import {
JSONSerializer,
Logger,
LogLevel,
RFC3339Stamper,
Serializer,
Stamper,
StdOutWriter,
Writer,
} from '..'

// helps us inspect the output of Logger without emitting to default std out
class FakeWriter {
Expand All @@ -15,56 +26,89 @@ const NoSerializer: Serializer = (msg: any) => msg
const FooStamper: Stamper = () => ({ foo: 'bar' })
const FazStamper: Stamper = () => ({ faz: 'baz' })

test(`should default to INFO level`, async () => {
expect(new Logger().level === LogLevel.INFO)
describe(`defaults`, () => {
const l = new Logger({})
test(`should default to INFO level`, () => {
assert.strictEqual(l.options.level, LogLevel.INFO)
})
test(`should default to JSONSerializer`, () => {
assert.strictEqual(l.options.serialize, JSONSerializer)
})
test(`should default to RFC3339Stamper`, () => {
assert.strictEqual(l.options.stamps.length, 1)
assert.strictEqual(l.options.stamps[0], RFC3339Stamper)
})
test(`should default to StdOutWriter`, () => {
assert.strictEqual(l.options.write, StdOutWriter)
})
})

describe(`when emitting logs of each level`, () => {
test(`should include level in output`, async () => {
test(`should include level in output`, () => {
// setup
const writer = new FakeWriter()
const logger = new Logger(LogLevel.DEBUG, writer.write, NoSerializer, [])
const logger = new Logger({
level: LogLevel.DEBUG,
serialize: NoSerializer,
write: writer.write,
stamps: [],
})

// run
logger.debug('foo')
expect(writer.message).toEqual({ message: 'foo', level: 'DEBUG' })
assert.deepEqual(writer.message, { message: 'foo', level: 'DEBUG' })
logger.info('foo')
expect(writer.message).toEqual({ message: 'foo', level: 'INFO' })
assert.deepEqual(writer.message, { message: 'foo', level: 'INFO' })
logger.warn('foo')
expect(writer.message).toEqual({ message: 'foo', level: 'WARN' })
assert.deepEqual(writer.message, { message: 'foo', level: 'WARN' })
logger.error('foo')
expect(writer.message).toEqual({ message: 'foo', level: 'ERROR' })
assert.deepEqual(writer.message, { message: 'foo', level: 'ERROR' })
})
})

describe(`when setting log level to DEBUG`, () => {
const writer = new FakeWriter()
const logger = new Logger(LogLevel.DEBUG, writer.write, NoSerializer, [])
const logger = new Logger({
level: LogLevel.DEBUG,
serialize: NoSerializer,
write: writer.write,
stamps: [],
})

test(`should log debug logs`, async () => {
test(`should log debug logs`, () => {
logger.debug('foo')
expect(writer.message).toEqual({ message: 'foo', level: 'DEBUG' })
assert.deepEqual(writer.message, { message: 'foo', level: 'DEBUG' })
})
})

describe(`when setting log level to INFO`, () => {
const writer = new FakeWriter()
const logger = new Logger(LogLevel.INFO, writer.write, NoSerializer, [])
const logger = new Logger({
level: LogLevel.INFO,
serialize: NoSerializer,
write: writer.write,
stamps: [],
})

test(`should not log debug logs`, async () => {
test(`should not log debug logs`, () => {
logger.debug('foo')
expect(writer.message).toBeUndefined()
assert.deepEqual(writer.message, undefined)
})
})

describe(`when adding context to a logger`, () => {
const writer = new FakeWriter()
const logger = new Logger(LogLevel.INFO, writer.write, NoSerializer, [])
const logger = new Logger({
level: LogLevel.DEBUG,
serialize: NoSerializer,
write: writer.write,
stamps: [],
})
const foologger = logger.with({ foo: 'bar' })

test(`should keep any context from earlier`, () => {
foologger.info('example', { faz: 'baz' })
expect(writer.message).toEqual({
assert.deepEqual(writer.message, {
message: 'example',
level: 'INFO',
foo: 'bar',
Expand All @@ -74,26 +118,36 @@ describe(`when adding context to a logger`, () => {

test(`should not impact parent logger`, () => {
logger.info('example')
expect(writer.message).toEqual({ message: 'example', level: 'INFO' })
assert.deepEqual(writer.message, { message: 'example', level: 'INFO' })
})
})

describe(`when given a "foo" stamper`, () => {
const writer = new FakeWriter()
const logger = new Logger(LogLevel.INFO, writer.write, NoSerializer, [FooStamper])
const logger = new Logger({
level: LogLevel.DEBUG,
serialize: NoSerializer,
write: writer.write,
stamps: [FooStamper],
})

test(`should stamp every message with foo`, () => {
logger.info('hi')
expect(writer.message).toEqual({ message: 'hi', level: 'INFO', foo: 'bar' })
assert.deepEqual(writer.message, { message: 'hi', level: 'INFO', foo: 'bar' })
})
})

describe(`when given a "foo" and "faz" stamper`, () => {
const writer = new FakeWriter()
const logger = new Logger(LogLevel.INFO, writer.write, NoSerializer, [FooStamper, FazStamper])
const logger = new Logger({
level: LogLevel.DEBUG,
serialize: NoSerializer,
write: writer.write,
stamps: [FooStamper, FazStamper],
})

test(`should stamp every message with foo and faz`, () => {
logger.info('hi')
expect(writer.message).toEqual({ message: 'hi', level: 'INFO', foo: 'bar', faz: 'baz' })
assert.deepEqual(writer.message, { message: 'hi', level: 'INFO', foo: 'bar', faz: 'baz' })
})
})
40 changes: 28 additions & 12 deletions src/Logger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,37 @@ export type Serializer = (msg: any) => string
// Stamper allows extention of the log event, you can sumply any number of stampers which will all apply their stamp to a message
export type Stamper = () => any

// Options contain all the required parts of the Logger
export type Options = {
level: LogLevel
serialize: Serializer
stamps: Stamper[]
write: Writer
}

// Parameters provides a way to instantiate a Logger with only some of the required parts
export interface Parameters extends Partial<Options> {}

// DefaultOptions declares the default options for a Logger
export const DefaultOptions: Options = {
level: LogLevel.INFO,
serialize: JSONSerializer,
stamps: [RFC3339Stamper],
write: StdOutWriter,
}

// Logger class allows consumer to instantiate a logger with any given level / writer
export class Logger {
// context holds any extra data to include in all logs from this logger
private context: any

constructor(
private readonly _level: LogLevel = LogLevel.INFO,
private readonly write: Writer = StdOutWriter,
private readonly serialize: Serializer = JSONSerializer,
private readonly stamps: Stamper[] = [RFC3339Stamper],
) {}
public readonly options: Options
constructor(params: Parameters) {
this.options = { ...DefaultOptions, ...params }
}
Comment on lines +48 to +50
Copy link
Owner Author

@pwmcintyre pwmcintyre May 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could optionally be a non-breaking change by checking the parameter type ... but it gets pretty ugly, i'd rather break the API 🤔

Suggested change
constructor(params: Parameters) {
this.options = { ...DefaultOptions, ...params }
}
constructor(
params?: LogLevel | Options,
write?: Writer,
serialize?: Serializer,
stamps?: Stamper[],
) {
if ( params === undefined ) {
this.options = { ...DefaultOptions }
} else if ( params === undefined && params as LogLevel in LogLevel ) {
this.options = { ...DefaultOptions }
this.options.level = params as LogLevel
this.options.write = write ? write : DefaultOptions.write
this.options.serialize = serialize ? serialize : DefaultOptions.serialize
this.options.stamps = stamps ? stamps : DefaultOptions.stamps
} else {
this.options = { ...DefaultOptions, ...params as Options }
}
}


// getters
public get level() {
return this._level
public get level(): LogLevel {
return this.options.level
}

// log level functions
Expand All @@ -58,7 +74,7 @@ export class Logger {
return
}

const stamps = this.stamps.reduce((agg, fn) => {
const stamps = this.options.stamps.reduce((agg, fn) => {
agg = { ...fn(), ...agg }
return agg
}, {})
Expand All @@ -71,12 +87,12 @@ export class Logger {
...extra,
}

this.write(this.serialize(log))
this.options.write(this.options.serialize(log))
}

// create a new logger, copy this loggers context merged with any new context
public with(context: any): Logger {
const l = new Logger(this.level, this.write, this.serialize, this.stamps)
const l = new Logger(this.options)
l.context = { ...this.context, ...context }
return l
}
Expand Down
9 changes: 6 additions & 3 deletions src/StandardLogger/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import assert from 'node:assert'
import { describe, test } from 'node:test'
import { StandardLogger } from '..'

test(`StandardLogger should be defined`, () => {
expect(StandardLogger).toBeDefined()
StandardLogger.info('example') // can't really assert anything here, just nice for humans to see
describe('StandardLogger', () => {
test(`should be defined`, () => {
assert.ok(StandardLogger)
})
})
4 changes: 3 additions & 1 deletion src/StandardLogger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Logger, LogLevel } from '..'
const level = (process.env.LOG_LEVEL || 'INFO') as keyof typeof LogLevel

// StandardLogger is a globally available Logger with a JSON Serializer, timstamps with RFC3339 timestamps, and writer to std out
export const StandardLogger = new Logger(LogLevel[level])
export const StandardLogger = new Logger({
level: LogLevel[level],
})

export default Logger
Loading