All files / backend/src/logging log-writer.ts

36% Statements 18/50
27.77% Branches 5/18
36.36% Functions 4/11
36.73% Lines 18/49

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140                    17x 17x 17x 17x     17x             21x 21x 21x 21x 21x                               4x 4x 4x             4x         4x     4x 3x     4x                                                                                                                                                      
/**
 * LogWriter - Handles writing log entries to JSONL files with daily rotation
 */
 
import fs from 'fs-extra';
import path from 'path';
import os from 'os';
import type { LogEntry } from './types.js';
 
export class LogWriter {
  private writeQueue: Array<{ entry: LogEntry; filePath: string }> = [];
  private flushTimeout: NodeJS.Timeout | null = null;
  private isWriting = false;
  private currentDate: string = '';
 
  constructor() {
    this.currentDate = this.getTodayDate();
  }
 
  /**
   * Get today's date in YYYY-MM-DD format
   */
  private getTodayDate(): string {
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, '0');
    const day = String(now.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }
 
  /**
   * Get the log file path for global application logs
   */
  getApplicationLogPath(date?: string): string {
    const logDate = date || this.getTodayDate();
    const logsDir = path.join(os.homedir(), 'Quiqr', 'logs');
    return path.join(logsDir, `application-${logDate}.jsonl`);
  }
 
  /**
   * Get the log file path for site-specific logs
   */
  getSiteLogPath(siteKey: string, workspaceKey: string, date?: string): string {
    const logDate = date || this.getTodayDate();
    const sitesDir = path.join(os.homedir(), 'Quiqr', 'sites');
    return path.join(sitesDir, siteKey, `${workspaceKey}-${logDate}.jsonl`);
  }
 
  /**
   * Write a log entry asynchronously
   */
  async write(entry: LogEntry): Promise<void> {
    const filePath = entry.type === 'global'
      ? this.getApplicationLogPath()
      : this.getSiteLogPath(entry.siteKey, entry.workspaceKey);
 
    // Add to queue
    this.writeQueue.push({ entry, filePath });
 
    // Schedule flush
    if (this.flushTimeout) {
      clearTimeout(this.flushTimeout);
    }
 
    this.flushTimeout = setTimeout(() => {
      this.flush().catch(err => {
        console.error('Failed to flush log queue:', err);
      });
    }, 100); // Batch writes every 100ms
  }
 
  /**
   * Flush the write queue to disk
   */
  private async flush(): Promise<void> {
    if (this.isWriting || this.writeQueue.length === 0) {
      return;
    }
 
    this.isWriting = true;
 
    try {
      // Check if date has changed (daily rotation)
      const today = this.getTodayDate();
      if (today !== this.currentDate) {
        this.currentDate = today;
      }
 
      // Group entries by file path
      const entriesByFile = new Map<string, LogEntry[]>();
      for (const { entry, filePath } of this.writeQueue) {
        if (!entriesByFile.has(filePath)) {
          entriesByFile.set(filePath, []);
        }
        entriesByFile.get(filePath)!.push(entry);
      }
 
      // Write to each file
      for (const [filePath, entries] of entriesByFile) {
        await this.writeToFile(filePath, entries);
      }
 
      // Clear the queue
      this.writeQueue = [];
    } finally {
      this.isWriting = false;
    }
  }
 
  /**
   * Write entries to a specific file
   */
  private async writeToFile(filePath: string, entries: LogEntry[]): Promise<void> {
    try {
      // Ensure directory exists
      await fs.ensureDir(path.dirname(filePath));
 
      // Convert entries to JSONL format
      const lines = entries.map(entry => JSON.stringify(entry)).join('\n') + '\n';
 
      // Append to file
      await fs.appendFile(filePath, lines, 'utf8');
    } catch (error) {
      console.error(`Failed to write to log file ${filePath}:`, error);
      throw error;
    }
  }
 
  /**
   * Force flush any pending writes (e.g., on shutdown)
   */
  async forceFlush(): Promise<void> {
    if (this.flushTimeout) {
      clearTimeout(this.flushTimeout);
      this.flushTimeout = null;
    }
    await this.flush();
  }
}