Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ dist/
# Dependency directories
node_modules/

# Caches
.eslintcache

# IDEs and editors
.idea/
.project/
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions packages/logger/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* eslint-env node */
module.exports = {
parserOptions: {
project: [
'./tsconfig.json',
'./tsconfig.jest.json',
],
},
}
8 changes: 8 additions & 0 deletions packages/logger/.lintstagedrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Reformat all .ts / .js
"**/*.(t|j)s":
- npm run prettier:staged
- npm run lint:staged

# sort package.json keys
./package.json:
- sort-package-json
30 changes: 30 additions & 0 deletions packages/logger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# logger

> 🎯 Target runtime: es2023 ([Node >= 20](https://node.green/#ES2023))

A simple logger to use with minimal dependencies.
By default, the logger is standalone and can be easily configured to log
messages to various transports.

# Usage

````typescript
import { Logger, LogLevel, LogTransport } from '@shiftcode/logger'

// Create a transport for logging to the console with a specific log level
const transport = new LogTransport(
LogLevel.DEBUG, // This controls the minimum log level
)

// LoggerService is used to manage loggers and their transports
const loggerService = new LoggerService([transport])

// Create a logger instance with a specific name and color
const logger = loggerService.getInstance('MyLogger', '#abcdef')

// Logging messages at different levels
logger.debug('This is a debug message')
logger.info('This is an info message')
logger.warn('This is a warning message')
logger.error('This is an error message')
````
18 changes: 18 additions & 0 deletions packages/logger/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-env node,es2023 */
// eslint-disable-next-line import/no-extraneous-dependencies
import { pathsToModuleNameMapper } from 'ts-jest'
import { readFileSync } from 'node:fs'

const tsConfig = JSON.parse(readFileSync('./tsconfig.jest.json', 'utf-8'))

export default {
testEnvironment: 'node',
extensionsToTreatAsEsm: ['.ts'],
transform: {
'^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.jest.json', useESM: true }],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths ?? {}, { prefix: '<rootDir>' }),
},
}
43 changes: 43 additions & 0 deletions packages/logger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@shiftcode/logger",
"version": "1.1.0-pr12.0",
"description": "logger for local and aws lambda execution",
"repository": "https://github.com/shiftcode/sc-commons-public",
"license": "UNLICENSED",
"author": "shiftcode GmbH <team@shiftcode.ch>",
"sideEffects": false,
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./test/*.js": {
"types": "./dist/test/*.d.ts",
"default": "./dist/test/*.js"
}
},
"scripts": {
"prebuild": "rm -rf ./dist",
"build": "tsc",
"lint": "npm run lint:src:fix && npm run lint:test:fix",
"lint:ci": "npm run lint:src && npm run lint:test",
"lint:src": "eslint ./src",
"lint:src:fix": "eslint ./src --cache --fix",
"lint:staged": "npm run lint",
"lint:test": "eslint ./test",
"lint:test:fix": "eslint ./test --cache --fix",
"prettier": "prettier --write --config ../../.prettierrc.yml '{src,e2e,test}/**/*.ts'",
"prettier:staged": "prettier --write --config ../../.prettierrc.yml",
"prepublish": "node ../publish-helper/dist/prepare-dist.js",
"test": "NODE_OPTIONS=\"--experimental-vm-modules --trace-warnings\" npx jest --config jest.config.js",
"test:ci": "npm run test",
"test:watch": "npm run test -- --watch"
},
"peerDependencies": {
"@shiftcode/utilities": "^3.0.0 || ^3.0.0-pr28.3"
},
"publishConfig": {
"directory": "dist"
}
}
9 changes: 9 additions & 0 deletions packages/logger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './model/logger.js'
export * from './model/log-level.enum.js'
export * from './model/log-transport.js'
export * from './model/json-log-transport.js'
export * from './model/json-log-object-data.js'

export * from './services/logger.service.js'

export * from './utils/logger-helper.js'
46 changes: 46 additions & 0 deletions packages/logger/src/model/json-log-object-data.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createJsonLogObjectData } from './json-log-object-data.js'
import { LogLevel } from './log-level.enum.js'

describe('createJsonLogObjectData', () => {
it('should create a log object with a message', () => {
const result = createJsonLogObjectData(LogLevel.INFO, 'MyClass', new Date('2023-01-01T00:00:00.000Z'), [
'Test message',
])

expect(result).toEqual({
level: 'INFO',
logger: 'MyClass',
timestamp: '2023-01-01T00:00:00.000Z',
message: 'Test message',
})
})

it('should create a log object with an error', () => {
const error = new Error('Test error')
const result = createJsonLogObjectData(LogLevel.ERROR, 'MyClass', new Date('2023-01-01T00:00:00.000Z'), [error])

expect(result).toEqual({
level: 'ERROR',
logger: 'MyClass',
timestamp: '2023-01-01T00:00:00.000Z',
message: 'Test error',
errorName: 'Error',
exception: error.stack,
})
})

it('should handle additional data', () => {
const result = createJsonLogObjectData(LogLevel.DEBUG, 'MyClass', new Date('2023-01-01T00:00:00.000Z'), [
'Test message',
{ key: 'value' },
])

expect(result).toEqual({
level: 'DEBUG',
logger: 'MyClass',
timestamp: '2023-01-01T00:00:00.000Z',
message: 'Test message',
data: { key: 'value' },
})
})
})
42 changes: 42 additions & 0 deletions packages/logger/src/model/json-log-object-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getEnumKeyFromNum } from '@shiftcode/utilities'
import { LogLevel } from './log-level.enum.js'

export interface JsonLogObjectData {
level: string
logger: string
timestamp: string /* ISO string */
message?: string
errorName?: string
exception?: string
data?: any[]
}

export function createJsonLogObjectData(
level: LogLevel,
clazzName: string,
timestamp: Date,
args: any[],
): JsonLogObjectData {
const logObjectData: Partial<JsonLogObjectData> = {
level: getEnumKeyFromNum(LogLevel, level),
timestamp: timestamp.toISOString(),
logger: clazzName,
}

const msgOrError = args.shift()
if (typeof msgOrError === 'string') {
logObjectData.message = msgOrError
} else if (msgOrError instanceof Error) {
logObjectData.message = msgOrError.message
logObjectData.errorName = msgOrError.name
logObjectData.exception = msgOrError.stack?.toString()
} else {
// first param is neither string nor error, put it back to args to pass it to data
args = [msgOrError, ...args]
}
if (args.length) {
logObjectData.data = args.length === 1 ? args[0] : args
}

return logObjectData as JsonLogObjectData
}
18 changes: 18 additions & 0 deletions packages/logger/src/model/json-log-transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { LogTransport } from './log-transport.js'
import { LogLevel } from './log-level.enum.js'
import { createJsonLogObjectData, JsonLogObjectData } from './json-log-object-data.js'

export abstract class JsonLogTransport extends LogTransport {
protected constructor(logLevel: LogLevel) {
super(logLevel)
}

log(level: LogLevel, clazzName: string, _hexColor: string, timestamp: Date, args: any[]) {
if (this.isLevelEnabled(level)) {
const logObject = createJsonLogObjectData(level, clazzName, timestamp, args)
this.transportLog(level, logObject)
}
}

abstract transportLog(level: LogLevel, logDataObject: JsonLogObjectData): void
}
7 changes: 7 additions & 0 deletions packages/logger/src/model/log-level.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum LogLevel {
DEBUG,
INFO,
WARN,
ERROR,
OFF,
}
48 changes: 48 additions & 0 deletions packages/logger/src/model/log-transport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { LogLevel } from '../index.js'
import { SpyLogTransport } from '../../test/spy-log.transport.js'
import { stringToColor } from '../utils/logger-helper.js'

describe('respects the configured level', () => {
test('respects level DEBUG', () => {
const logger = new SpyLogTransport(LogLevel.DEBUG)
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
expect(logger.mock).toHaveBeenCalledTimes(4)
expect(logger.calls[0][0]).toBe(LogLevel.DEBUG)
expect(logger.calls[1][0]).toBe(LogLevel.INFO)
expect(logger.calls[2][0]).toBe(LogLevel.WARN)
expect(logger.calls[3][0]).toBe(LogLevel.ERROR)
})
test('respects level INFO', () => {
const logger = new SpyLogTransport(LogLevel.INFO)
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
expect(logger.mock).toHaveBeenCalledTimes(3)
expect(logger.calls[0][0]).toBe(LogLevel.INFO)
expect(logger.calls[1][0]).toBe(LogLevel.WARN)
expect(logger.calls[2][0]).toBe(LogLevel.ERROR)
})
test('respects level WARN', () => {
const logger = new SpyLogTransport(LogLevel.WARN)
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
expect(logger.mock).toHaveBeenCalledTimes(2)
expect(logger.calls[0][0]).toBe(LogLevel.WARN)
expect(logger.calls[1][0]).toBe(LogLevel.ERROR)
})
test('respects level ERROR', () => {
const logger = new SpyLogTransport(LogLevel.ERROR)
logger.log(LogLevel.DEBUG, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar debug'])
logger.log(LogLevel.INFO, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar info'])
logger.log(LogLevel.WARN, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar warn'])
logger.log(LogLevel.ERROR, 'MyClass', stringToColor('MyClass'), new Date(), ['foo bar error'])
expect(logger.mock).toHaveBeenCalledTimes(1)
expect(logger.calls[0][0]).toBe(LogLevel.ERROR)
})
})
15 changes: 15 additions & 0 deletions packages/logger/src/model/log-transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { LogLevel } from './log-level.enum.js'

export abstract class LogTransport {
protected logLevel: LogLevel

protected constructor(logLevel: LogLevel) {
this.logLevel = logLevel
}

abstract log(level: LogLevel, clazzName: string, hexColor: string, timestamp: Date, args: any[]): void

protected isLevelEnabled(level: LogLevel) {
return level >= this.logLevel
}
}
29 changes: 29 additions & 0 deletions packages/logger/src/model/logger.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Logger, LogLevel } from '../index.js'
import { SpyLogTransport } from '../../test/spy-log.transport.js'
import { stringToColor } from '../utils/logger-helper.js'

describe('Logger behavior', () => {
let logger: Logger
let spyTransport1: SpyLogTransport
let spyTransport2: SpyLogTransport
const className = 'TestLogger'
const color = stringToColor(className)

it('should send logs to all transports', () => {
spyTransport1 = new SpyLogTransport(LogLevel.DEBUG)
spyTransport2 = new SpyLogTransport(LogLevel.INFO)
logger = new Logger(className, color, [spyTransport1, spyTransport2])
logger.error(['foo bar'])
expect(spyTransport1.calls.length).toBe(1)
expect(spyTransport2.calls.length).toBe(1)
})

it('should respect log level', () => {
spyTransport1 = new SpyLogTransport(LogLevel.DEBUG)
spyTransport2 = new SpyLogTransport(LogLevel.INFO)
logger = new Logger(className, color, [spyTransport1, spyTransport2])
logger.debug(['foo bar'])
expect(spyTransport1.calls[0][0]).toBe(LogLevel.DEBUG)
expect(spyTransport2.calls.length).toBe(0)
})
})
Loading