/ utils / diagLogs.ts
diagLogs.ts
 1  import { dirname } from 'path'
 2  import { getFsImplementation } from './fsOperations.js'
 3  import { jsonStringify } from './slowOperations.js'
 4  
 5  type DiagnosticLogLevel = 'debug' | 'info' | 'warn' | 'error'
 6  
 7  type DiagnosticLogEntry = {
 8    timestamp: string
 9    level: DiagnosticLogLevel
10    event: string
11    data: Record<string, unknown>
12  }
13  
14  /**
15   * Logs diagnostic information to a logfile. This information is sent
16   * via the environment manager to session-ingress to monitor issues from
17   * within the container.
18   *
19   * *Important* - this function MUST NOT be called with any PII, including
20   * file paths, project names, repo names, prompts, etc.
21   *
22   * @param level    Log level. Only used for information, not filtering
23   * @param event    A specific event: "started", "mcp_connected", etc.
24   * @param data     Optional additional data to log
25   */
26  // sync IO: called from sync context
27  export function logForDiagnosticsNoPII(
28    level: DiagnosticLogLevel,
29    event: string,
30    data?: Record<string, unknown>,
31  ): void {
32    const logFile = getDiagnosticLogFile()
33    if (!logFile) {
34      return
35    }
36  
37    const entry: DiagnosticLogEntry = {
38      timestamp: new Date().toISOString(),
39      level,
40      event,
41      data: data ?? {},
42    }
43  
44    const fs = getFsImplementation()
45    const line = jsonStringify(entry) + '\n'
46    try {
47      fs.appendFileSync(logFile, line)
48    } catch {
49      // If append fails, try creating the directory first
50      try {
51        fs.mkdirSync(dirname(logFile))
52        fs.appendFileSync(logFile, line)
53      } catch {
54        // Silently fail if logging is not possible
55      }
56    }
57  }
58  
59  function getDiagnosticLogFile(): string | undefined {
60    return process.env.CLAUDE_CODE_DIAGNOSTICS_FILE
61  }
62  
63  /**
64   * Wraps an async function with diagnostic timing logs.
65   * Logs `{event}_started` before execution and `{event}_completed` after with duration_ms.
66   *
67   * @param event   Event name prefix (e.g., "git_status" -> logs "git_status_started" and "git_status_completed")
68   * @param fn      Async function to execute and time
69   * @param getData Optional function to extract additional data from the result for the completion log
70   * @returns       The result of the wrapped function
71   */
72  export async function withDiagnosticsTiming<T>(
73    event: string,
74    fn: () => Promise<T>,
75    getData?: (result: T) => Record<string, unknown>,
76  ): Promise<T> {
77    const startTime = Date.now()
78    logForDiagnosticsNoPII('info', `${event}_started`)
79  
80    try {
81      const result = await fn()
82      const additionalData = getData ? getData(result) : {}
83      logForDiagnosticsNoPII('info', `${event}_completed`, {
84        duration_ms: Date.now() - startTime,
85        ...additionalData,
86      })
87      return result
88    } catch (error) {
89      logForDiagnosticsNoPII('error', `${event}_failed`, {
90        duration_ms: Date.now() - startTime,
91      })
92      throw error
93    }
94  }