Skip to content

[Feature] Keep log file open in FileHandler to avoid repeated syscalls #99

@vmarcella

Description

@vmarcella

Overview

The FileHandler currently re-opens the log file on each flush, resulting in unnecessary syscalls and potential contention. This issue proposes mirroring the JsonHandler implementation by keeping a Mutex<BufWriter<File>> open for the lifetime of the handler, writing and flushing without reopening.

Current State

The FileHandler reopens the log file on every flush:

// crates/lambda-rs-logging/src/handler.rs:71-79
let mut file = OpenOptions::new()
  .append(true)
  .create(true)
  .open(self.file.clone())  // Reopened on every flush
  .unwrap();

file
  .write_all(log_message.as_bytes())
  .expect("Unable to write data");

This results in:

  • Repeated open() syscalls on every log flush
  • Potential file descriptor exhaustion under heavy logging
  • Contention when multiple threads attempt to log simultaneously
  • Unnecessary overhead compared to keeping a persistent file handle

In contrast, JsonHandler already maintains a persistent file handle wrapped in Mutex<BufWriter<File>>, demonstrating the preferred pattern.

Scope

Goals:

  • Refactor FileHandler to keep the log file open for the handler's lifetime
  • Use Mutex<BufWriter<File>> pattern consistent with JsonHandler
  • Reduce syscall overhead and improve logging throughput
  • Maintain thread-safety for concurrent logging

Non-Goals:

  • Log rotation (separate concern, can be built on top of this)
  • Async file I/O
  • Changing the public FileHandler API

Proposed API

No public API changes required. The refactoring is internal to FileHandler:

// crates/lambda-rs-logging/src/handler.rs

use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::sync::Mutex;

pub struct FileHandler {
  /// Persistent buffered file handle
  writer: Mutex<BufWriter<File>>,
  // ... other fields
}

impl FileHandler {
  pub fn new(path: impl AsRef<Path>) -> std::io::Result<Self> {
    let file = OpenOptions::new()
      .create(true)
      .append(true)
      .open(path)?;
    
    Ok(Self {
      writer: Mutex::new(BufWriter::new(file)),
    })
  }
}

impl Handler for FileHandler {
  fn handle(&self, record: &Record) {
    let formatted = self.format(record);
    if let Ok(mut writer) = self.writer.lock() {
      let _ = writeln!(writer, "{}", formatted);
      // Flush can be deferred or done periodically for better performance
    }
  }
  
  fn flush(&self) {
    if let Ok(mut writer) = self.writer.lock() {
      let _ = writer.flush();
    }
  }
}

Acceptance Criteria

  • FileHandler stores a Mutex<BufWriter<File>> instead of reopening on each flush
  • File is opened once during FileHandler construction
  • handle() method writes to the buffered writer without reopening
  • flush() method flushes the buffered writer without reopening
  • Thread-safety maintained via Mutex
  • Existing tests pass without modification
  • Error handling for file operations is preserved or improved

Affected Crates

lambda-rs-logging

Notes

  • Consider whether flush() should be called after every handle() or batched for better performance
  • File handle will be held for the lifetime of the handler; document that log rotation requires handler recreation

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions