Changing logger to work with AsyncLocalStorage to propagate logger to callee functions

This commit is contained in:
Collin Duncan 2022-11-30 11:56:39 +01:00
parent 420aa5ed78
commit 02327fc734
No known key found for this signature in database
7 changed files with 95 additions and 121 deletions

View file

@ -1,39 +1,14 @@
import { AsyncLocalStorage } from 'async_hooks'
export enum LogLevel { export enum LogLevel {
DEBUG, DEBUG,
INFO, INFO,
ERROR, ERROR,
} }
export const asyncLocalStorage = new AsyncLocalStorage<Logger>()
export class Logger { export class Logger {
private static instance: LoggerInstance
public static instantiate(
tag: string,
correlationId: string,
level = LogLevel.ERROR
): LoggerInstance {
Logger.instance = new LoggerInstance(tag, correlationId, level)
return Logger.instance
}
public static getInstance(): LoggerInstance {
return Logger.instance
}
public static debug(message: string, details?: unknown): void {
Logger.getInstance().debug(message, details)
}
public static info(message: string, details?: unknown): void {
Logger.getInstance().info(message, details)
}
public static error(message: string, details?: unknown): void {
Logger.getInstance().error(message, details)
}
}
export class LoggerInstance {
private readonly tag: string private readonly tag: string
private readonly correlationId: string private readonly correlationId: string
private readonly level: LogLevel private readonly level: LogLevel

View file

@ -1,6 +1,6 @@
import argon2 from 'argon2' import argon2 from 'argon2'
import crypto from 'crypto' import crypto from 'crypto'
import { Logger } from './logger' import { asyncLocalStorage } from './logger'
const SALT_LENGTH = Number.parseInt(process.env.SALT_LENGTH || '32', 10) const SALT_LENGTH = Number.parseInt(process.env.SALT_LENGTH || '32', 10)
@ -42,7 +42,7 @@ export const hashPassword = async (password: string) => {
const hash = await generateHash(password, saltBuffer) const hash = await generateHash(password, saltBuffer)
return hash return hash
} catch (err: any) { } catch (err: any) {
Logger.error('Error hashing and salting password', { message: err.message }) asyncLocalStorage.getStore()?.error('Error hashing and salting password', { message: err.message })
throw err throw err
} }
} }

View file

@ -7,7 +7,7 @@ import puppeteer, {
LaunchOptions, LaunchOptions,
Page, Page,
} from 'puppeteer' } from 'puppeteer'
import { Logger } from './logger' import { asyncLocalStorage } from './logger'
import { Opponent, Reservation } from './reservation' import { Opponent, Reservation } from './reservation'
export class Runner { export class Runner {
@ -26,7 +26,7 @@ export class Runner {
} }
public async run(reservation: Reservation): Promise<boolean> { public async run(reservation: Reservation): Promise<boolean> {
Logger.debug('Launching browser') asyncLocalStorage.getStore()?.debug('Launching browser')
if (!this.browser) { if (!this.browser) {
this.browser = await puppeteer.launch(this.options) this.browser = await puppeteer.launch(this.options)
} }
@ -36,7 +36,7 @@ export class Runner {
} }
private async login(username: string, password: string) { private async login(username: string, password: string) {
Logger.debug('Logging in') asyncLocalStorage.getStore()?.debug('Logging in')
await this.page?.goto('https://squashcity.baanreserveren.nl/') await this.page?.goto('https://squashcity.baanreserveren.nl/')
await this.page await this.page
?.waitForSelector('input[name=username]') ?.waitForSelector('input[name=username]')
@ -54,7 +54,7 @@ export class Runner {
reservation.booked = true reservation.booked = true
return true return true
} catch (err) { } catch (err) {
Logger.error('Error making reservation', reservation.format()) asyncLocalStorage.getStore()?.error('Error making reservation', reservation.format())
return false return false
} }
} }
@ -74,10 +74,10 @@ export class Runner {
} }
private async navigateToDay(date: Dayjs): Promise<void> { private async navigateToDay(date: Dayjs): Promise<void> {
Logger.debug(`Navigating to ${date.format()}`) asyncLocalStorage.getStore()?.debug(`Navigating to ${date.format()}`)
if (this.getLastVisibleDay().isBefore(date)) { if (this.getLastVisibleDay().isBefore(date)) {
Logger.debug('Date is on different page, increase month') asyncLocalStorage.getStore()?.debug('Date is on different page, increase month')
await this.page?.waitForSelector('td.month.next').then((d) => d?.click()) await this.page?.waitForSelector('td.month.next').then((d) => d?.click())
} }
@ -96,7 +96,7 @@ export class Runner {
} }
private async selectAvailableTime(res: Reservation): Promise<void> { private async selectAvailableTime(res: Reservation): Promise<void> {
Logger.debug('Selecting available time', res.format()) asyncLocalStorage.getStore()?.debug('Selecting available time', res.format())
let freeCourt: ElementHandle | null | undefined let freeCourt: ElementHandle | null | undefined
let i = 0 let i = 0
while (i < res.possibleDates.length && !freeCourt) { while (i < res.possibleDates.length && !freeCourt) {
@ -116,7 +116,7 @@ export class Runner {
} }
private async selectOpponent(opponent: Opponent): Promise<void> { private async selectOpponent(opponent: Opponent): Promise<void> {
Logger.debug('Selecting opponent', opponent) asyncLocalStorage.getStore()?.debug('Selecting opponent', opponent)
const player2Search = await this.page?.waitForSelector( const player2Search = await this.page?.waitForSelector(
'tr.res-make-player-2 > td > input' 'tr.res-make-player-2 > td > input'
) )

View file

@ -1,7 +1,6 @@
import { Dayjs } from 'dayjs' import { Dayjs } from 'dayjs'
import { v4 } from 'uuid'
import { Logger, LogLevel } from './logger' import { asyncLocalStorage } from './logger'
import { Reservation } from './reservation' import { Reservation } from './reservation'
import { validateJSONRequest } from './request' import { validateJSONRequest } from './request'
import { reserve } from './reserver' import { reserve } from './reserver'
@ -20,23 +19,23 @@ export type SchedulerInput = Record<string, unknown>
export const schedule = async ( export const schedule = async (
payload: SchedulerInput payload: SchedulerInput
): Promise<SchedulerResult> => { ): Promise<SchedulerResult> => {
Logger.instantiate('scheduler', v4(), LogLevel.DEBUG) const logger = asyncLocalStorage.getStore()
Logger.debug('Handling reservation', { payload }) logger?.debug('Handling reservation', { payload })
let reservation: Reservation let reservation: Reservation
try { try {
reservation = await validateJSONRequest(payload) reservation = await validateJSONRequest(payload)
} catch (err) { } catch (err) {
Logger.error('Failed to validate request', { err }) logger?.error('Failed to validate request', { err })
throw err throw err
} }
Logger.debug('Successfully validated request', { logger?.debug('Successfully validated request', {
reservation: reservation.format(), reservation: reservation.format(),
}) })
if (!reservation.isAvailableForReservation()) { if (!reservation.isAvailableForReservation()) {
Logger.debug( logger?.debug(
'Reservation date is more than 7 days away; saving for later reservation' 'Reservation date is more than 7 days away; saving for later reservation'
) )
@ -50,7 +49,7 @@ export const schedule = async (
} }
} }
Logger.info('Reservation request can be performed now') logger?.info('Reservation request can be performed now')
await reserve(reservation) await reserve(reservation)
return { return {
scheduledReservation: { reservation }, scheduledReservation: { reservation },

View file

@ -1,5 +1,6 @@
import { schedule, ScheduledTask, ScheduleOptions } from 'node-cron' import { schedule, ScheduledTask, ScheduleOptions } from 'node-cron'
import { Logger } from '../common/logger' import { v4 } from 'uuid'
import { asyncLocalStorage, Logger, LogLevel } from '../common/logger'
import { reserve } from '../common/reserver' import { reserve } from '../common/reserver'
const tasks: ScheduledTask[] = [] const tasks: ScheduledTask[] = []
@ -10,33 +11,38 @@ const getTaskConfig = (name: string): ScheduleOptions => ({
timezone: 'Europe/Amsterdam', timezone: 'Europe/Amsterdam',
}) })
const logger = new Logger('cron', 'default', LogLevel.DEBUG)
export const startTasks = () => { export const startTasks = () => {
try { try {
const task = schedule( const task = schedule(
'0 * * * * *', '0 * * * * *',
async (timestamp) => { async (timestamp) => {
Logger.info('Running cron job', { timestamp }) asyncLocalStorage.run(new Logger('cron', v4(), LogLevel.DEBUG), async () => {
const childLogger = asyncLocalStorage.getStore()
childLogger?.info('Running cron job', { timestamp })
try { try {
await reserve() await reserve()
Logger.info('Completed running cron job') childLogger?.info('Completed running cron job')
} catch (error: any) { } catch (error: any) {
Logger.error('Error running cron job', { error: error.message }) childLogger?.error('Error running cron job', { error: error.message })
} }
})
}, },
getTaskConfig('reserver cron') getTaskConfig('reserver cron')
) )
Logger.debug('Started cron task') logger.debug('Started cron task')
tasks.push(task) tasks.push(task)
} catch (error: any) { } catch (error: any) {
Logger.error('Failed to start tasks', { error: error.message }) logger.error('Failed to start tasks', { error: error.message })
} }
} }
export const stopTasks = () => { export const stopTasks = () => {
try { try {
tasks.map((task) => task.stop()) tasks.map((task) => task.stop())
Logger.debug('Stopped cron tasks') logger.debug('Stopped cron tasks')
} catch (error: any) { } catch (error: any) {
Logger.error('Failed to stop tasks', { error: error.message }) logger.error('Failed to stop tasks', { error: error.message })
} }
} }

View file

@ -1,23 +1,23 @@
import http from 'http' import http from 'http'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { Logger, LogLevel } from '../common/logger' import { asyncLocalStorage, Logger, LogLevel } from '../common/logger'
import { schedule } from '../common/scheduler' import { schedule } from '../common/scheduler'
import { parseJson } from './utils' import { parseJson } from './utils'
// Handles POST requests to /reservations // Handles POST requests to /reservations
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
await asyncLocalStorage.run(new Logger('request', v4(), LogLevel.DEBUG), async () => {
const logger = asyncLocalStorage.getStore()
logger?.debug('Incoming request')
const { url, method } = req const { url, method } = req
Logger.instantiate('request', v4(), LogLevel.DEBUG)
Logger.debug('Incoming request')
if ( if (
!url || !url ||
!method || !method ||
!/^\/reservations$/.test(url) || !/^\/reservations$/.test(url) ||
method.toLowerCase() !== 'post' method.toLowerCase() !== 'post'
) { ) {
Logger.info('Not found') logger?.info('Not found')
res.writeHead(404, 'Not found') res.writeHead(404, 'Not found')
res.end() res.end()
return return
@ -26,7 +26,7 @@ const server = http.createServer(async (req, res) => {
let jsonBody: Record<string, unknown> let jsonBody: Record<string, unknown>
const contentType = req.headers['content-type'] || 'application/json' const contentType = req.headers['content-type'] || 'application/json'
if (contentType !== 'application/json') { if (contentType !== 'application/json') {
Logger.error('Invalid content type', { contentType }) logger?.error('Invalid content type', { contentType })
res.writeHead(406, 'Unsupported content type') res.writeHead(406, 'Unsupported content type')
res.end() res.end()
return return
@ -37,7 +37,7 @@ const server = http.createServer(async (req, res) => {
const encoding = req.readableEncoding || 'utf8' const encoding = req.readableEncoding || 'utf8'
jsonBody = await parseJson(length, encoding, req) jsonBody = await parseJson(length, encoding, req)
} catch (error: any) { } catch (error: any) {
Logger.error('Failed to parse body', { error: error.message }) logger?.error('Failed to parse body', { error: error.message })
res.writeHead(400, 'Bad request') res.writeHead(400, 'Bad request')
res.end() res.end()
return return
@ -46,7 +46,7 @@ const server = http.createServer(async (req, res) => {
try { try {
await schedule(jsonBody) await schedule(jsonBody)
} catch (error: any) { } catch (error: any) {
Logger.error('Failed to schedule request', { error }) logger?.error('Failed to schedule request', { error })
res.writeHead(400, 'Bad request') res.writeHead(400, 'Bad request')
res.end() res.end()
return return
@ -54,5 +54,6 @@ const server = http.createServer(async (req, res) => {
res.end() res.end()
}) })
})
export default server export default server

View file

@ -5,23 +5,16 @@ describe('Logger', () => {
jest.resetAllMocks() jest.resetAllMocks()
}) })
test('should create a single instance of LoggerInstance', () => {
const a = Logger.instantiate('tag', 'abc', LogLevel.DEBUG)
const b = Logger.getInstance()
expect(a).toStrictEqual(b)
})
test('should log messages', () => { test('should log messages', () => {
const consoleLogSpy = jest.fn() const consoleLogSpy = jest.fn()
const consoleErrorSpy = jest.fn() const consoleErrorSpy = jest.fn()
jest.spyOn(console, 'log').mockImplementation(consoleLogSpy) jest.spyOn(console, 'log').mockImplementation(consoleLogSpy)
jest.spyOn(console, 'error').mockImplementation(consoleErrorSpy) jest.spyOn(console, 'error').mockImplementation(consoleErrorSpy)
Logger.instantiate('tag', 'abc', LogLevel.DEBUG) const logger = new Logger('tag', 'abc', LogLevel.DEBUG)
Logger.debug('first') logger.debug('first')
Logger.info('second') logger.info('second')
Logger.error('third', { errorMessage: 'test' }) logger.error('third', { errorMessage: 'test' })
expect(consoleLogSpy).toHaveBeenCalledTimes(2) expect(consoleLogSpy).toHaveBeenCalledTimes(2)
expect(consoleLogSpy).toHaveBeenNthCalledWith( expect(consoleLogSpy).toHaveBeenNthCalledWith(
@ -55,8 +48,8 @@ describe('Logger', () => {
const consoleLogSpy = jest.fn() const consoleLogSpy = jest.fn()
jest.spyOn(console, 'log').mockImplementationOnce(consoleLogSpy) jest.spyOn(console, 'log').mockImplementationOnce(consoleLogSpy)
Logger.instantiate('tag', 'abc', LogLevel.INFO) const logger = new Logger('tag', 'abc', LogLevel.INFO)
Logger.debug("should't appear") logger.debug("shouldn't appear")
expect(consoleLogSpy).not.toHaveBeenCalled() expect(consoleLogSpy).not.toHaveBeenCalled()
}) })
@ -67,8 +60,8 @@ describe('Logger', () => {
jest.spyOn(console, 'log').mockImplementation(consoleLogSpy) jest.spyOn(console, 'log').mockImplementation(consoleLogSpy)
jest.spyOn(console, 'error').mockImplementation(consoleErrorSpy) jest.spyOn(console, 'error').mockImplementation(consoleErrorSpy)
Logger.instantiate('tag', 'abc', LogLevel.DEBUG) const logger = new Logger('tag', 'abc', LogLevel.DEBUG)
Logger.info('first', { password: 'test' }) logger.info('first', { password: 'test' })
expect(consoleLogSpy).toHaveBeenCalledTimes(1) expect(consoleLogSpy).toHaveBeenCalledTimes(1)
expect(consoleLogSpy).toHaveBeenNthCalledWith( expect(consoleLogSpy).toHaveBeenNthCalledWith(