Skip to content

Commit

Permalink
Merge pull request #38 from logtracing/add-log-reporter
Browse files Browse the repository at this point in the history
WIP: Add a new class called LogReporter that works as an API to access to the stored logs
  • Loading branch information
vcgtz committed Jul 13, 2023
2 parents 0707038 + 2e24103 commit 14d8c47
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 9 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"scripts": {
"build": "tsc",
"build": "rm -rf dist/ && tsc",
"db:migrate": "sequelize-cli db:migrate",
"test": "NODE_ENV=test npm run db:migrate && jest",
"test": "NODE_ENV=test npm run db:migrate && jest --verbose",
"tsc:init": "tsc --init",
"tsc": "tsc"
},
Expand Down
134 changes: 134 additions & 0 deletions src/LogReporter.ts
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', ' ')}`;
}
}
2 changes: 2 additions & 0 deletions src/index.ts
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,
};
31 changes: 31 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @ts-ignore
import { LogGroup } from './db/models/index';
export type PrepareStackTrace = ((err: Error, stackTraces: NodeJS.CallSite[]) => any) | undefined;

// General types
Expand Down Expand Up @@ -146,3 +148,32 @@ export interface ExtraDetailsModelData {
isJson: boolean,
errorExceptionId: number,
}

export interface ModelSearchQuery {
limit: number,
offset: number,
where?: {
[identifier: string]: any,
},
order?: string[][],
include?: object[],
}

export interface LogReporterOptions {
limit?: number,
offset?: number,
level?: LogType,
groupName?: LogGroup
}

export interface LogReporterSegments {
[identifier: string]: any,
}

export interface LogReporterObject {
flow: string,
datetime: string,
level: LogType,
content: string,
group?: LogGroup,
}
143 changes: 143 additions & 0 deletions tests/logReporter.test.ts
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,
}
});
});
});
Loading

0 comments on commit 14d8c47

Please sign in to comment.