@nxtwebmasters/nxt-smart-logger
Version:
A configurable smart logger for web apps that supports server and Google Tag Manager (GTM) logging with user/session context.
236 lines (206 loc) • 6.39 kB
text/typescript
// src/ConsoleInterceptor.ts
import { v4 as uuidv4 } from 'uuid';
declare global {
interface Window {
dataLayer?: any[];
}
}
export interface StructuredLog {
timestamp: string;
level: string;
message: string;
tags?: string[];
context: Record<string, any>;
meta: {
url: string;
userAgent?: string;
traceId?: string;
sessionId?: string;
[key: string]: any;
};
data?: any;
}
export class ConsoleInterceptor {
private logQueue: StructuredLog[] = [];
private batchSize: number;
private flushInterval: number;
private enableGTM: boolean;
private enableServer: boolean;
private contextProvider: () => Record<string, any>;
private serverLogger: ((logs: StructuredLog[]) => Promise<void>) | null;
private originalConsole: any;
private customLevels: string[];
private logger: Record<string, any> = {};
private defaultSessionId: string = uuidv4();
private generateTraceId: () => string = () => uuidv4();
private persistentContext: Record<string, any> = {};
private persistentTags: string[] = [];
constructor(options: {
batchSize?: number;
flushInterval?: number;
enableGTM?: boolean;
enableServer?: boolean;
contextProvider?: () => Record<string, any>;
serverLogger?: (logs: StructuredLog[]) => Promise<void>;
customLevels?: string[];
generateTraceId?: () => string;
filterLevels?: string[];
} = {}) {
this.batchSize = options.batchSize || 5;
this.flushInterval = options.flushInterval || 5000;
this.enableGTM = options.enableGTM ?? true;
this.enableServer = options.enableServer ?? true;
this.contextProvider = options.contextProvider || (() => ({}));
this.serverLogger = options.serverLogger || null;
this.customLevels = (
options.filterLevels?.length
? ['log', 'error', 'warn', 'info', 'debug'].filter(lvl => options.filterLevels!.includes(lvl))
: ['log', 'error', 'warn', 'info', 'debug']
).concat(options.customLevels || []);
if (options.generateTraceId) {
this.generateTraceId = options.generateTraceId;
}
this.originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info,
debug: console.debug
};
this.overrideConsole();
this.setupFlushInterval();
this.setupCustomLogger();
}
overrideConsole() {
for (const level of ['log', 'error', 'warn', 'info', 'debug'] as (keyof Console)[]) {
(console as any)[level] = (...args: any[]) => {
this.originalConsole[level].apply(console, args);
this.enqueueLog(level, args);
};
}
}
setupCustomLogger() {
for (const level of this.customLevels) {
this.logger[level] = (...args: any[]) => {
if (this.originalConsole[level]) {
this.originalConsole[level].apply(console, args);
}
this.enqueueLog(level, args);
};
}
this.logger.withTags = (tags: string[]) => {
return Object.fromEntries(
this.customLevels.map(level => [
level,
(...args: any[]) => {
this.enqueueLog(level, args, tags);
}
])
);
};
this.logger.withContext = (extraContext: Record<string, any>) => {
return Object.fromEntries(
this.customLevels.map(level => [
level,
(...args: any[]) => {
this.enqueueLog(level, args, undefined, extraContext);
}
])
);
};
this.logger.withMeta = (extraMeta: Record<string, any>) => {
return Object.fromEntries(
this.customLevels.map(level => [
level,
(...args: any[]) => {
this.enqueueLog(level, args, undefined, undefined, extraMeta);
}
])
);
};
this.logger.withAll = ({ context, tags, meta }: {
context?: Record<string, any>;
tags?: string[];
meta?: Record<string, any>;
}) => {
return Object.fromEntries(
this.customLevels.map(level => [
level,
(...args: any[]) => {
this.enqueueLog(level, args, tags, context, meta);
}
])
);
};
}
setContext(ctx: Record<string, any>) {
this.persistentContext = { ...this.persistentContext, ...ctx };
}
clearContext() {
this.persistentContext = {};
}
clearTags() {
this.persistentTags = [];
}
getLogger() {
return this.logger;
}
enqueueLog(
level: string,
args: any[],
tags?: string[],
extraContext: Record<string, any> = {},
extraMeta: Record<string, any> = {}
) {
const dynamicContext = this.contextProvider();
const context = { ...this.persistentContext, ...dynamicContext, ...extraContext };
const sessionId = context.sessionId || this.defaultSessionId;
const traceId = context.traceId || this.generateTraceId();
const meta = {
url: window.location.href,
userAgent: navigator.userAgent,
sessionId,
traceId,
...extraMeta
};
let data: any = null;
const messageParts: string[] = [];
for (const arg of args) {
if (typeof arg === 'object' && arg !== null) {
data = { ...data, ...arg };
messageParts.push(JSON.stringify(arg));
} else {
messageParts.push(String(arg));
}
}
const log: StructuredLog = {
level,
timestamp: new Date().toISOString(),
message: messageParts.join(' '),
tags: tags || this.persistentTags,
context,
meta,
data
};
this.logQueue.push(log);
if (this.enableGTM && window.dataLayer) {
window.dataLayer.push({ event: 'console_log', log });
}
if (this.logQueue.length >= this.batchSize) {
this.flushLogs();
}
}
setupFlushInterval() {
setInterval(() => this.flushLogs(), this.flushInterval);
}
async flushLogs() {
if (!this.enableServer || !this.serverLogger || this.logQueue.length === 0) return;
const logsToSend = [...this.logQueue];
this.logQueue = [];
try {
await this.serverLogger(logsToSend);
} catch (err) {
this.logQueue.unshift(...logsToSend);
}
}
}