-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from logtracing/add-log-reporter
WIP: Add a new class called LogReporter that works as an API to access to the stored logs
- Loading branch information
Showing
6 changed files
with
319 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
import { LogReporterOptions, ModelSearchQuery, LogReporterSegments, LogReporterObject } from './types'; | ||
// @ts-ignore | ||
import { Log, LogGroup } from './db/models/index'; | ||
import { Op } from 'sequelize'; | ||
|
||
export default class LogReporter { | ||
private static DEFAULT_LIMIT = 50; | ||
private static DEFAULT_OFFSET = 0; | ||
|
||
private flow: string; | ||
|
||
constructor(flow: string) { | ||
this.flow = flow; | ||
} | ||
|
||
getBasicLogs(options: LogReporterOptions = {}): Promise<string[]> { | ||
return new Promise((resolve, reject) => { | ||
const query: ModelSearchQuery = { | ||
limit: options.limit ?? LogReporter.DEFAULT_LIMIT, | ||
offset: options.offset ?? LogReporter.DEFAULT_OFFSET, | ||
where: { | ||
flow: { | ||
[Op.eq]: this.flow, | ||
} | ||
}, | ||
order: [ | ||
['createdAt', 'DESC'], | ||
] | ||
}; | ||
|
||
if (options.level) { | ||
query.where!.level = { | ||
[Op.eq]: options.level, | ||
}; | ||
} | ||
|
||
if (options.groupName) { | ||
query.where = { | ||
...query.where, | ||
...{ | ||
'$LogGroup.name$': { | ||
[Op.eq]: options.groupName.toLowerCase(), | ||
}, | ||
} | ||
}; | ||
|
||
query.include = [{ | ||
model: LogGroup, | ||
as: 'LogGroup' | ||
}] | ||
} | ||
|
||
Log.findAll(query) | ||
.then((data: any) => resolve( | ||
data.map((log: Log) => { | ||
const segments = { | ||
group: options.groupName ?? null, | ||
}; | ||
|
||
return this.format(log, segments); | ||
}) | ||
)) | ||
.catch((err: any) => reject(err)); | ||
}); | ||
} | ||
|
||
getLogs(options: LogReporterOptions = {}): Promise<LogReporterObject[]> { | ||
return new Promise((resolve, reject) => { | ||
const query: ModelSearchQuery = { | ||
limit: options.limit ?? LogReporter.DEFAULT_LIMIT, | ||
offset: options.offset ?? LogReporter.DEFAULT_OFFSET, | ||
where: { | ||
flow: { | ||
[Op.eq]: this.flow, | ||
} | ||
}, | ||
order: [ | ||
['createdAt', 'DESC'], | ||
], | ||
include: [ | ||
{ model: LogGroup, as: 'LogGroup' } | ||
], | ||
}; | ||
|
||
if (options.level) { | ||
query.where!.level = { | ||
[Op.eq]: options.level, | ||
}; | ||
} | ||
|
||
if (options.groupName) { | ||
query.where = { | ||
...query.where, | ||
...{ | ||
'$LogGroup.name$': { | ||
[Op.eq]: options.groupName.toLowerCase(), | ||
}, | ||
} | ||
}; | ||
} | ||
|
||
Log.findAll(query) | ||
.then((data: any) => resolve(data.map((log: Log) => { | ||
return { | ||
flow: this.flow, | ||
datetime: this.formatDate(log.createdAt), | ||
level: log.level, | ||
content: log.content, | ||
group: log.LogGroup ? log.LogGroup.name : null, | ||
}; | ||
}))) | ||
.catch((err: any) => reject(err)); | ||
}); | ||
} | ||
|
||
private format(log: Log, segments: LogReporterSegments = {}): string { | ||
const newSegments: string[] = [ | ||
`[${log.level.padEnd(5)}]`, | ||
`[${this.formatDate(log.createdAt)}]`, | ||
]; | ||
|
||
for (const segment in segments) { | ||
if (segments[segment]) { | ||
newSegments.push(`[${segments[segment]}]`); | ||
} | ||
} | ||
|
||
return `${newSegments.join('')}: ${log.content}`; | ||
} | ||
|
||
private formatDate(date: Date): string { | ||
return `${date.toJSON().slice(0, 19).replace('T', ' ')}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,11 @@ | ||
import ExceptionLogger from './ExceptionLogger'; | ||
import Logger from './Logger'; | ||
import LogReporter from './LogReporter'; | ||
import { LogType } from './types'; | ||
|
||
export { | ||
ExceptionLogger, | ||
Logger, | ||
LogReporter, | ||
LogType, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { expect, describe, test, afterAll, beforeAll } from '@jest/globals'; | ||
import { Logger, LogReporter } from '../src/index'; | ||
// @ts-ignore | ||
import { Log, LogGroup } from '../src/db/models/index'; | ||
import { LogReporterObject } from '../src/types'; | ||
|
||
describe('Tests for the LogReporter class and its simple logs', () => { | ||
let flow: string; | ||
let groupName: string; | ||
let content: string; | ||
|
||
beforeAll(async () => { | ||
flow = `${Date.now()}`; | ||
groupName = `${Date.now()}-group`; | ||
content = `${Date.now()}-content`; | ||
|
||
const logger: Logger = new Logger(flow); | ||
|
||
await logger.trace(`${content}-trace`); | ||
await logger.info(`${content}-info`); | ||
await logger.debug(`${content}-debug`); | ||
await logger.warn(`${content}-warn`); | ||
await logger.error(`${content}-error`); | ||
await logger.fatal(`${content}-fatal`); | ||
await logger.warn(`${content}-war2`); | ||
await logger.error(`${content}-error2`); | ||
await logger.fatal(`${content}-fatal2`); | ||
}); | ||
|
||
test('should return the total of the stored logs', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getBasicLogs(); | ||
|
||
expect(logs.length).toBe(9); | ||
}); | ||
|
||
test('should return a limited amount of logs according to the passed options', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getBasicLogs({ | ||
limit: 3, | ||
}); | ||
|
||
expect(logs.length).toBe(3); | ||
}); | ||
|
||
test('should return an array of strings', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getBasicLogs(); | ||
|
||
expect(logs.every(l => typeof(l) === 'string')).toBe(true); | ||
}); | ||
|
||
test('should return an array of strings that matches with the expected format', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getBasicLogs(); | ||
const regEx = /\[(TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\s{0,1}\]\[\d{4}-\d{2}-\d{2}\s{1}\d{2}:\d{2}:\d{2}\]:[\w\d\s]*/ | ||
|
||
expect(logs.every(l => regEx.test(l))).toBe(true); | ||
}); | ||
|
||
afterAll(async () => { | ||
await Log.destroy({ | ||
where: { | ||
flow, | ||
} | ||
}) | ||
}); | ||
}); | ||
|
||
describe('Tests for the LogReporter class and its complex logs', () => { | ||
let flow: string; | ||
let groupName: string; | ||
let content: string; | ||
|
||
beforeAll(async () => { | ||
flow = `${Date.now()}`; | ||
groupName = `${Date.now()}-group`; | ||
content = `${Date.now()}-content`; | ||
|
||
const logger: Logger = new Logger(flow); | ||
const group = await logger.getOrCreateGroup(groupName); | ||
|
||
await logger.trace(`${content}-trace`); | ||
await logger.info(`${content}-info`); | ||
await logger.debug(`${content}-debug`); | ||
await logger.warn(`${content}-warn`); | ||
await logger.error(`${content}-error`); | ||
await logger.fatal(`${content}-fatal`); | ||
await logger.warn(`${content}-war2`, { group }); | ||
await logger.error(`${content}-error2`, { group }); | ||
await logger.fatal(`${content}-fatal2`, { group }); | ||
}); | ||
|
||
test('should return the total of the stored logs', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getLogs(); | ||
|
||
expect(logs.length).toBe(9); | ||
}); | ||
|
||
test('should return a limited amount of logs according to the passed options', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getLogs({ | ||
limit: 3, | ||
}); | ||
|
||
expect(logs.length).toBe(3); | ||
}); | ||
|
||
test('should return an array of LogReporterObject objects', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getLogs(); | ||
|
||
const isLogReporterObject = (obj: LogReporterObject): obj is LogReporterObject => { | ||
return (obj as LogReporterObject).flow !== undefined; | ||
} | ||
|
||
expect(logs.every(l => isLogReporterObject(l))).toBe(true); | ||
}); | ||
|
||
test('should filter logs objects by a group name', async () => { | ||
const reporter: LogReporter = new LogReporter(flow); | ||
const logs = await reporter.getLogs({ | ||
groupName, | ||
}); | ||
|
||
expect(logs.length).toBe(3); | ||
}); | ||
|
||
afterAll(async () => { | ||
await Log.destroy({ | ||
where: { | ||
flow, | ||
} | ||
}); | ||
|
||
await LogGroup.destroy({ | ||
where: { | ||
name: groupName, | ||
} | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.