logger.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import * as fs from 'node:fs';
  2. import { appendFile } from 'node:fs/promises';
  3. import * as os from 'node:os';
  4. import * as path from 'node:path';
  5. const LOG_PREFIX = 'oh-my-opencode-slim.';
  6. const LOG_SUFFIX = '.log';
  7. const RETENTION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
  8. let logFile: string | null = null;
  9. let writeChain: Promise<void> = Promise.resolve();
  10. function getLogDir(): string {
  11. return (
  12. process.env.OPENCODE_LOG_DIR ??
  13. path.join(os.homedir(), '.local/share/opencode')
  14. );
  15. }
  16. function cleanupOldLogs(logDir: string): void {
  17. try {
  18. const entries = fs.readdirSync(logDir);
  19. const now = Date.now();
  20. for (const entry of entries) {
  21. if (entry.startsWith(LOG_PREFIX) && entry.endsWith(LOG_SUFFIX)) {
  22. const filePath = path.join(logDir, entry);
  23. try {
  24. const stat = fs.statSync(filePath);
  25. if (now - stat.mtimeMs > RETENTION_MS) {
  26. fs.unlinkSync(filePath);
  27. }
  28. } catch {
  29. // Skip individual file errors
  30. }
  31. }
  32. }
  33. } catch {
  34. // Directory may not exist yet — that's fine
  35. }
  36. // Apply the same 7-day retention to persisted background task files
  37. try {
  38. const bgTaskDir = path.join(logDir, 'bg-tasks');
  39. const taskFiles = fs.readdirSync(bgTaskDir);
  40. const now = Date.now();
  41. for (const entry of taskFiles) {
  42. if (!entry.endsWith('.json')) continue;
  43. const filePath = path.join(bgTaskDir, entry);
  44. try {
  45. const stat = fs.statSync(filePath);
  46. if (now - stat.mtimeMs > RETENTION_MS) {
  47. fs.unlinkSync(filePath);
  48. }
  49. } catch {
  50. // Skip individual file errors
  51. }
  52. }
  53. } catch {
  54. // bg-tasks dir may not exist yet — that's fine
  55. }
  56. }
  57. export function initLogger(sessionId: string): void {
  58. const dir = getLogDir();
  59. try {
  60. fs.mkdirSync(dir, { recursive: true });
  61. } catch {
  62. // Directory creation failed — logging will silently fail
  63. }
  64. logFile = path.join(dir, `${LOG_PREFIX}${sessionId}${LOG_SUFFIX}`);
  65. try {
  66. fs.closeSync(fs.openSync(logFile, 'a'));
  67. } catch {
  68. // File creation failed — later writes will silently fail
  69. }
  70. cleanupOldLogs(dir);
  71. }
  72. /** @internal Reset logger state for testing */
  73. export function resetLogger(): void {
  74. logFile = null;
  75. writeChain = Promise.resolve();
  76. }
  77. /** @internal Wait for queued log writes in tests. */
  78. export async function flushLoggerForTesting(): Promise<void> {
  79. await writeChain;
  80. }
  81. export { getLogDir };
  82. export function log(message: string, data?: unknown): void {
  83. const target = logFile;
  84. if (!target) return; // Uninitialized — silently no-op
  85. try {
  86. const timestamp = new Date().toISOString();
  87. let dataStr = '';
  88. if (data !== undefined) {
  89. try {
  90. dataStr = JSON.stringify(data);
  91. } catch {
  92. dataStr = '[unserializable]';
  93. }
  94. }
  95. const logEntry = `[${timestamp}] ${message} ${dataStr}\n`;
  96. writeChain = writeChain
  97. .then(() => appendFile(target, logEntry))
  98. .catch(() => {
  99. // Silently ignore logging errors and keep future writes alive
  100. });
  101. } catch {
  102. // Silently ignore logging errors
  103. }
  104. }