Skip to main content
Glama
audit-log-persistence.ts4.63 kB
import { Clock } from "@mcpx/toolkit-core/time"; import fs from "fs"; import { DateTime } from "luxon"; import path from "path"; import { Logger } from "winston"; import { LOG_FLAGS } from "../../log-flags.js"; import { AuditLog } from "../../model/audit-log-type.js"; export interface AuditLogPersistence { persist(events: AuditLog[]): Promise<void>; cleanup(): Promise<void>; } export class FileAuditLogPersistence implements AuditLogPersistence { constructor( private readonly auditLogDir: string, private readonly retentionHours: number, private readonly clock: Clock, private readonly logger: Logger, ) { this.ensureDirectoryExists(); } async persist(events: AuditLog[]): Promise<void> { if (events.length === 0) { return; } // Clean up old files before persisting new events await this.cleanup(); // Group events by hour for efficient batching const eventsByHour = new Map<string, string[]>(); const persistTime = this.clock.now(); // Pre-serialize all events to avoid repeated JSON.stringify calls for (const event of events) { event.createdAt = persistTime; const hourKey = this.getHourKey(event.timestamp); const currentHourEvents = eventsByHour.get(hourKey) || []; currentHourEvents.push(JSON.stringify(event)); eventsByHour.set(hourKey, currentHourEvents); } // Write each hour group efficiently const writePromises = Array.from(eventsByHour.entries()).map( async ([hourKey, jsonLines]) => { const filename = this.getFilenameForHour(hourKey); const filepath = path.join(this.auditLogDir, filename); try { await fs.promises.access(filepath).catch(async () => { await fs.promises.writeFile(filepath, ""); }); // Single write operation with all lines concatenated const content = jsonLines.join("\n") + "\n"; await fs.promises.appendFile(filepath, content); if (LOG_FLAGS.LOG_AUDIT_LOG_PERSISTENCE) { this.logger.debug( `Persisted ${jsonLines.length} events to ${filename}`, ); } } catch (error) { this.logger.error(`Failed to persist events to ${filename}`, { error, }); throw error; } }, ); // Wait for all writes to complete await Promise.all(writePromises); } async cleanup(): Promise<void> { try { const cutoffTime = DateTime.fromJSDate(this.clock.now()) .minus({ hours: this.retentionHours }) .toJSDate(); const files = await this.getAuditLogFiles(); let deletedCount = 0; for (const file of files) { const fileTime = this.parseTimeFromFilename(file); if (!fileTime) continue; if (fileTime < cutoffTime) { try { await fs.promises.unlink(path.join(this.auditLogDir, file)); deletedCount++; this.logger.debug(`Deleted old audit log file: ${file}`); } catch (error) { this.logger.warn(`Failed to delete old audit log file: ${file}`, { error, }); } } } if (deletedCount > 0) { this.logger.info(`Cleaned up ${deletedCount} old audit log files`); } } catch (error) { this.logger.error("Error during audit log cleanup", { error }); } } private async ensureDirectoryExists(): Promise<void> { try { await fs.promises.access(this.auditLogDir); } catch { await fs.promises.mkdir(this.auditLogDir, { recursive: true }); this.logger.debug(`Created audit log directory: ${this.auditLogDir}`); } } private getHourKey(timestamp: Date): string { return timestamp.toISOString().slice(0, 13); // YYYY-MM-DDTHH } private getFilenameForHour(hourKey: string): string { return `audit-${hourKey}.jsonl`; } private async getAuditLogFiles(): Promise<string[]> { try { return await fs.promises.readdir(this.auditLogDir); } catch (error) { this.logger.warn("Failed to read audit log directory", { error }); return []; } } private parseTimeFromFilename(filename: string): Date | null { try { // Extract time from filename like "audit-2025-07-25T13.jsonl" const match = filename.match(/audit-(\d{4}-\d{2}-\d{2}T\d{2})\.jsonl/); if (match) { return new Date(match[1] + ":00:00.000Z"); } } catch (error) { this.logger.warn(`Failed to parse time from filename: ${filename}`, { error, }); } return null; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TheLunarCompany/lunar'

If you have feedback or need assistance with the MCP directory API, please join our Discord server