WIP Logging service and API.

This commit is contained in:
2019-03-02 02:04:51 -06:00
parent fc8dbd3fb7
commit e9bdcbffcd
16 changed files with 331 additions and 28 deletions

View File

@ -3,11 +3,16 @@ import App from './App.vue';
import router from './router';
import store from './store';
import './registerServiceWorker';
import { LogService, LogLevel, ApiAppender, ConsoleAppender } from './services/logging';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
Vue.component('fa-icon', FontAwesomeIcon);
Vue.config.productionTip = false;
LogService.getRootLogger().appenders.push(
// TODO: prod/dev config settings for logging?
new ConsoleAppender(LogLevel.DEBUG),
new ApiAppender(process.env.VUE_APP_PM_API_BASE + '/log/batch', '', LogLevel.WARN)
);
new Vue({
router,

View File

@ -0,0 +1,65 @@
import Axios from 'axios';
import { LogMessage, LogLevel } from './log-message';
import Logger from './logger';
import LogAppender from './log-appender';
interface ApiMessage {
level: string;
message: string;
scope: string;
stacktrace: string;
timestamp: string;
}
export class ApiAppender implements LogAppender {
public batchSize = 10;
public minimumTimePassedInSec = 60;
public maximumTimePassedInSec = 120;
private http = Axios.create();
private msgBuffer: ApiMessage[] = [];
private lastSent = 0;
constructor(public readonly apiEndpoint: string, public authToken: string, public threshold?: LogLevel) {
setInterval(this.checkPost, 1000);
}
public appendMessage(logger: Logger, msg: LogMessage): void {
if (this.threshold && msg.level < this.threshold) { return; }
this.msgBuffer.push({
level: LogLevel[msg.level],
message: msg.message,
scope: logger.name,
stacktrace: msg.stacktrace,
timestamp: msg.timestamp.toISOString()
});
}
private checkPost = () => {
const now = Date.now();
const min = this.lastSent + (this.minimumTimePassedInSec * 1000);
const max = this.lastSent + (this.maximumTimePassedInSec * 1000);
if ( (this.msgBuffer.length >= this.batchSize && min < now) ||
(this.msgBuffer.length > 0 && max < now) ) {
this.doPost();
}
setInterval(this.checkPost, Math.max(10000, this.minimumTimePassedInSec * 1000));
}
private doPost() {
if (this.msgBuffer.length === 0) { return; }
this.http.post(this.apiEndpoint, this.msgBuffer,
{ headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.authToken}`
}});
this.lastSent = Date.now();
this.msgBuffer = [];
}
}
export default ApiAppender;

View File

@ -0,0 +1,30 @@
/*tslint:disable:no-console*/
import { LogMessage, LogLevel} from './log-message';
import Logger from './logger';
import LogAppender from './log-appender';
export class ConsoleAppender implements LogAppender {
constructor(public threshold?: LogLevel) {}
public appendMessage(logger: Logger, msg: LogMessage): void {
if (this.threshold && msg.level < this.threshold) { return; }
let logMethod = console.log;
switch (msg.level) {
case LogLevel.ALL: logMethod = console.trace; break;
case LogLevel.DEBUG: logMethod = console.debug; break;
case LogLevel.INFO: logMethod = console.info; break;
case LogLevel.WARN: logMethod = console.warn; break;
case LogLevel.ERROR:
case LogLevel.FATAL: logMethod = console.trace; break;
}
if (msg.error) {
logMethod(logger.name, msg.message, msg.error);
} else {
logMethod(logger.name, msg.message, msg.stacktrace);
}
}
}
export default ConsoleAppender;

View File

@ -0,0 +1,5 @@
export * from './log-message';
export * from './log-appender';
export * from './log-service';
export * from './console-appender';
export * from './api-appender';

View File

@ -0,0 +1,5 @@
import { LogLevel, LogMessage } from './log-message';
import Logger from './logger';
export default interface LogAppender {
appendMessage(logger: Logger, message: LogMessage): void;
}

View File

@ -0,0 +1,11 @@
export enum LogLevel { ALL = 0, DEBUG, INFO, WARN, ERROR, FATAL }
export interface LogMessage {
level: LogLevel;
message: string;
stacktrace: string;
error?: Error;
timestamp: Date;
}
export default LogMessage;

View File

@ -0,0 +1,33 @@
import { LogLevel } from './log-message';
import Logger from './logger';
import { default as Axios, AxiosInstance } from 'axios';
/* tslint:disable:max-classes-per-file*/
export class LogService {
public static getRootLogger(): Logger { return Logger.ROOT_LOGGER; }
private loggers: { [key: string]: Logger } = { };
private http: AxiosInstance = Axios.create();
public getLogger(name: string, threshold?: LogLevel): Logger {
if (this.loggers.hasOwnProperty(name)) { return this.loggers[name]; }
let parentLogger: Logger;
const parentLoggerName = Object.keys(this.loggers)
.filter((n: string) => name.startsWith(n))
.reduce((acc: string, cur: string) => acc.length > cur.length ? acc : cur);
if (parentLoggerName) {
parentLogger = this.loggers[parentLoggerName];
} else {
parentLogger = Logger.ROOT_LOGGER;
}
this.loggers[name] = parentLogger.createChildLogger(name, threshold);
return this.loggers[name];
}
}
export default new LogService();

View File

@ -0,0 +1,66 @@
import { LogMessage, LogLevel } from './log-message';
import LogAppender from './log-appender';
export default class Logger {
public static readonly ROOT_LOGGER = new Logger('ROOT', undefined, LogLevel.ALL);
public appenders: LogAppender[] = [];
protected constructor(public readonly name: string, private parentLogger?: Logger, public threshold?: LogLevel) { }
public createChildLogger(name: string, threshold?: LogLevel): Logger {
return new Logger(name, this, threshold);
}
public log(level: LogLevel, message: (Error | string), stacktrace?: string): void {
if (level < this.getEffectiveThreshold()) { return; }
const logMsg: LogMessage = { level, message: '', stacktrace: '', timestamp: new Date() };
if (typeof message === 'string') {
logMsg.message = message;
logMsg.stacktrace = stacktrace == null ? '' : stacktrace;
} else {
logMsg.error = message;
logMsg.message = `${message.name}: ${message.message}`;
logMsg.stacktrace = message.stack == null ? '' : message.stack;
}
this.appenders.forEach((app) => {
app.appendMessage(this, logMsg);
});
}
public debug(message: (Error | string), stacktrace?: string): void {
this.log(LogLevel.DEBUG, message, stacktrace);
}
public info(message: string, stacktrace?: string): void {
this.log(LogLevel.INFO, message, stacktrace);
}
public warn(message: string, stacktrace?: string): void {
this.log(LogLevel.WARN, message, stacktrace);
}
public error(message: string, stacktrace?: string): void {
this.log(LogLevel.ERROR, message, stacktrace);
}
public fatal(message: string, stacktrace?: string): void {
this.log(LogLevel.FATAL, message, stacktrace);
}
protected getEffectiveThreshold(): LogLevel {
if (this.threshold) { return this.threshold; }
if (this.parentLogger) { return this.parentLogger.getEffectiveThreshold(); }
// should never happen (root logger should always have a threshold
return LogLevel.ALL;
}
}