Skip to content

fix(desktop): add size rotation and retention policy to debug logger#494

Open
HiddenPuppy wants to merge 1 commit into
mainfrom
fix/logger-rotation-retention
Open

fix(desktop): add size rotation and retention policy to debug logger#494
HiddenPuppy wants to merge 1 commit into
mainfrom
fix/logger-rotation-retention

Conversation

@HiddenPuppy
Copy link
Copy Markdown
Collaborator

Closes #411

Changes

  • CreateDebugLoggerOptions — added optional maxFileSize (default 50 MB) and retentionDays (default 14).
  • Size-based rotation — when a daily log exceeds maxFileSize, subsequent writes go to debug-YYYY-MM-DD.1.ndjson, .2.ndjson, etc.
  • Age-based cleanup — on logger init, files older than retentionDays are purged from debugDir.
  • Keep it simple — no new dependencies, pure Node.js fs API.
  • Tests — rotation threshold trigger, cleanup old files, disabled cleanup, size boundary.

- Add maxFileSize option (default 50MB) for daily file rotation
- Add retentionDays option (default 14) for old file cleanup
- Update CreateDebugLoggerOptions interface
- Add unit tests for rotation and cleanup behavior

Closes #411
@github-actions
Copy link
Copy Markdown
Contributor

Hi @HiddenPuppy,
Thanks for your pull request!
If the PR is ready, use the /auto-cc command to assign Reviewer to Review.
We will review it shortly.

Details

Instructions for interacting with me using comments are available here.
If you have questions or suggestions related to my behavior, please file an issue against the gh-ci-bot repository.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the desktop debug logger by introducing log rotation and retention management. These changes prevent log files from growing indefinitely and ensure that old logs are automatically pruned, improving disk space usage and maintainability without introducing external dependencies.

Highlights

  • Configuration Updates: Added maxFileSize and retentionDays options to CreateDebugLoggerOptions to control log file size and cleanup policies.
  • Size-based Rotation: Implemented logic to rotate log files when they exceed the defined maxFileSize, creating numbered suffixes (e.g., .1.ndjson).
  • Automated Cleanup: Added a cleanup mechanism that purges log files older than the specified retentionDays upon logger initialization.
  • Testing: Added comprehensive test coverage for rotation triggers, retention cleanup, and file size boundary conditions.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements log rotation and retention for the debug logger, adding support for file size limits and automatic purging of logs older than a specified retention period. The review feedback identifies a potential race condition on Windows where renaming a file immediately after closing an asynchronous write stream may fail due to file locks. Additionally, it is recommended to wrap the synchronous cleanup logic in a try-catch block to prevent application crashes during initialization and to optimize the cleanup process by using lexicographical string comparisons instead of repeated Date object instantiation.

Comment on lines +118 to +125
writeStream.end();
writeStream = null;

let rotIdx = 1;
while (existsSync(join(debugDir, `debug-${today}.${rotIdx}.ndjson`))) {
rotIdx++;
}
renameSync(currentFilePath(), join(debugDir, `debug-${today}.${rotIdx}.ndjson`));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The writeStream.end() call is asynchronous. On Windows, calling renameSync immediately after end() will likely fail with an EBUSY or EPERM error because the file descriptor is still held open by the stream until it finishes flushing its internal buffer.

Since this is a synchronous logging flow, you should consider using fs.appendFileSync for the entire logger to simplify rotation, or you must ensure the stream is fully closed before renaming. However, WriteStream does not provide a synchronous close method. A common workaround in Electron apps for simple debug logging is to avoid WriteStream and use manual file descriptor management with fs.writeSync and fs.closeSync.

Comment on lines +213 to +225
function cleanupOldLogs(dir: string, retentionDays: number): void {
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isFile()) continue;
const match = entry.name.match(/^debug-(\d{4}-\d{2}-\d{2})(?:\.\d+)?\.ndjson$/);
if (!match) continue;
const fileDate = new Date(match[1]).getTime();
if (fileDate < cutoff) {
unlinkSync(join(dir, entry.name));
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The cleanupOldLogs function performs synchronous I/O operations (readdirSync, unlinkSync) on the main thread during initialization. If the log directory contains a very large number of files or if there are permission issues, this could block the Electron main process and delay app startup. It is recommended to wrap these operations in a try...catch block to prevent a minor cleanup failure from crashing the entire application.

function cleanupOldLogs(dir: string, retentionDays: number): void {
  try {
    const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
    const entries = readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      if (!entry.isFile()) continue;
      const match = entry.name.match(/^debug-(\d{4}-\d{2}-\d{2})(?:\.\d+)?\.ndjson$/);
      if (!match) continue;
      const fileDate = new Date(match[1]).getTime();
      if (fileDate < cutoff) {
        try {
          unlinkSync(join(dir, entry.name));
        } catch (err) {
          console.error("[debug] failed to delete old log " + entry.name + ":", err);
        }
      }
    }
  } catch (err) {
    console.error("[debug] log cleanup failed:", err);
  }
}

Comment on lines +220 to +221
const fileDate = new Date(match[1]).getTime();
if (fileDate < cutoff) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Creating a new Date object for every file in the directory is inefficient. Since the log filenames use the YYYY-MM-DD format, you can perform a lexicographical string comparison instead. This is faster and avoids potential timezone edge cases during date parsing.

Suggested change
const fileDate = new Date(match[1]).getTime();
if (fileDate < cutoff) {
const cutoffDate = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
if (match[1] < cutoffDate) {

Copy link
Copy Markdown
Collaborator

@mvanhorn mvanhorn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HiddenPuppy nice work on this, especially the 247-line test file. Two of gemini's three points are worth fixing; the third is a nit.

  1. (high, logger.ts:125) - Windows rename race is real. writeStream.end() is async and the fd stays open until flush completes; renameSync immediately after will throw EBUSY/EPERM on Windows. Ubuntu CI won't catch it because POSIX permits renaming open files. Simplest fix: drop the rename entirely. When rotation triggers, just open the next .N.ndjson slot and write there - leave the current file as-is. The naming already supports this (debug-YYYY-MM-DD.ndjson, .1.ndjson, .2.ndjson...) and natural-sort reads back in order.

  2. (medium, logger.ts:225) - Agree, wrap cleanupOldLogs in try/catch. A perm or race issue during init shouldn't crash the main process. Per-unlinkSync try/catch is also worth it so one stuck file doesn't abort the loop.

  3. (medium, logger.ts:221) - This is a nit. The lex YYYY-MM-DD comparison is a perf win at very large file counts and avoids the (minor) new Date('2026-05-15') UTC-midnight interpretation, but the current code isn't broken. Take it or leave it.

CI is fully green here unlike #491, which is the main reason I'd land this once 1+2 are addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] debug logger has no size rotation or retention policy

2 participants