This commit is contained in:
Bojan Kucera 2025-06-03 15:26:21 -04:00
parent 4397541d2c
commit 4ab83d1dc2
5 changed files with 89 additions and 8 deletions

View file

@ -6,7 +6,6 @@
"declaration": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"module": "ESNext",
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,3 @@
export * from './dateUtils';
export * from './logger';
export * from './lokiClient';
export * from './lokiClient.ts';

View file

@ -2,15 +2,14 @@
* Logger utility with consistent formatting and log levels
* Supports console and Loki logging
*/
import { loggingConfig } from '@stock-bot/config';
import { LokiClient } from './lokiClient';
import { loggingConfig, lokiConfig } from '@stock-bot/config';
// Singleton Loki client
let lokiClient: LokiClient | null = null;
function getLokiClient(): LokiClient {
if (!lokiClient) {
lokiClient = new LokiClient(loggingConfig);
lokiClient = new LokiClient();
}
return lokiClient;
}
@ -45,7 +44,7 @@ export class Logger {
const logMessage = `[${timestamp}] [${levelStr}] [${this.serviceName}] ${fullMessage}`;
// Console logging
if (loggingConfig.console) {
if (loggingConfig.LOG_CONSOLE) {
switch (level) {
case LogLevel.ERROR:
console.error(logMessage);
@ -106,3 +105,85 @@ export enum LogLevel {
export function createLogger(serviceName: string, level: LogLevel = LogLevel.INFO): Logger {
return new Logger(serviceName, level);
}
export class LokiClient {
private batchQueue: any[] = [];
private flushInterval: NodeJS.Timeout;
private lokiUrl: string;
private authHeader?: string;
constructor() {
const { LOKI_HOST, LOKI_PORT, LOKI_USERNAME, LOKI_PASSWORD } = lokiConfig;
this.lokiUrl = `http://${LOKI_HOST}:${LOKI_PORT}/loki/api/v1/push`;
if (LOKI_USERNAME && LOKI_PASSWORD) {
const authString = Buffer.from(`${LOKI_USERNAME}:${LOKI_PASSWORD}`).toString('base64');
this.authHeader = `Basic ${authString}`;
}
this.flushInterval = setInterval(
() => this.flush(),
lokiConfig.LOKI_FLUSH_INTERVAL_MS || 1000 // Default to 1 second if not set
);
}
async log(level: string, message: string, serviceName: string, labels: Record<string, string> = {}) {
const timestamp = Date.now() * 1000000; // Loki expects nanoseconds
this.batchQueue.push({
streams: [{
stream: {
level,
service: serviceName,
...lokiConfig.LOKI_DEFAULT_LABELS ? JSON.parse(lokiConfig.LOKI_DEFAULT_LABELS) : {},
...labels,
},
values: [[`${timestamp}`, message]],
}],
});
if (this.batchQueue.length >= lokiConfig.LOKI_BATCH_SIZE) {
await this.flush();
}
}
private async flush() {
if (this.batchQueue.length === 0) return;
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (this.authHeader) {
headers['Authorization'] = this.authHeader;
}
const response = await fetch(this.lokiUrl, {
method: 'POST',
headers,
body: JSON.stringify({
streams: this.batchQueue.flatMap(batch => batch.streams),
}),
});
if (!response.ok) {
console.error(`Failed to send logs to Loki: ${response.status} ${response.statusText}`);
const text = await response.text();
if (text) {
console.error(text);
}
}
} catch (error) {
console.error('Error sending logs to Loki:', error);
} finally {
this.batchQueue = [];
}
}
async destroy() {
clearInterval(this.flushInterval);
return this.flush();
}
}

View file

@ -5,6 +5,8 @@
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*"],
"include": [
"src/**/*"
],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}