How to rotate logs of unrealengine application?

Summary

This postmortem addresses a critical logging misconfiguration in long-running Unreal Engine applications (LiveLink/MetaHuman projects) that results in unbounded log file growth, potential disk exhaustion, and inability to rotate active log files due to Windows file handle locking. The core issue is that Unreal Engine opens log files in exclusive write mode without built-in rotation support, while Windows lacks native logrotate functionality. The fix involves implementing log rotation at the application layer using Unreal’s logging hooks, or using external Windows-native tools like logrotate.exe for Windows or PowerShell-based rotation scripts that can gracefully handle locked files by leveraging Unreal’s Core.Log verbosity controls and restart mechanisms.

Root Cause

Unreal Engine’s logging subsystem opens the primary log file (UE4.log, UE5.log, or project-specific logs) with exclusive file locking from application start, preventing external tools from modifying or moving the file while the engine runs. This is a deliberate design choice for performance and data integrity, but creates operational challenges for long-running services.

  • Exclusive File Locking: Windows file system semantics prevent renaming or deleting a file while a process holds an open handle with write privileges
  • No Built-in Rotation: Unreal Engine lacks native log rotation hooks in its core logging pipeline (unlike log4j or glog)
  • Buffered Writes: Log messages are buffered and flushed asynchronously, making it unsafe to abruptly close file handles
  • Startup Log Locking: The engine also creates a separate startup log (Startup.log) with similar locking behavior
  • No SIGHUP Equivalent: Windows services cannot receive Unix-style signals to trigger rotation without process restart

Why This Happens in Real Systems

Long-running Unreal processes (LiveLink servers, rendering farms, MetaHuman streaming instances) are typically treated as daemon-like services, but the engine was originally architected for short-lived editor sessions and game runs. In production deployments:

  • Continuous Operation: These apps run 24/7, causing logs to grow from MBs to GBs over days
  • No Process Restarts: Stability requirements prevent frequent restarts, eliminating the “rotate on startup” workaround
  • Locked Ecosystem: Windows enterprise environments often lack Unix-style background utilities, forcing reliance on custom scripting
  • Telemetry Dependencies: Logs are ingested by SIEM tools; rotation is needed for proper log forwarding and retention policies
  • Disk Quotas: Shared infrastructure enforces strict disk limits; a single runaway log can crash multiple services

Real-World Impact

Failure to rotate logs manifests as production incidents with cascading effects:

  • Disk Exhaustion: A 50GB log file can fill a 100GB volume in under 48 hours under heavy LiveLink traffic
  • Application Crashes: Unreal Engine will fail to write when disk space hits 0%, causing silent data loss and session corruption
  • Log Ingestion Failure: SIEM agents (Splunk, Elasticsearch) may stop tailing oversized files or miss rotations
  • Performance Degradation: File I/O contention slows the main thread when flushing multi-gigabyte log buffers
  • Security Blind Spots: Overflowing logs discard audit trails required for compliance (e.g., HIPAA, SOC2)
  • Operational Overhead: Engineers must manually SSH/RDP to kill processes and delete logs, causing downtime

Example or Code

For Windows-native rotation without modifying Unreal Engine, use PowerShell to safely rotate by leveraging Unreal’s log verbosity to “close” the file handle temporarily. This script should be run as a scheduled task.

param(
    [string]$LogPath = "C:\Path\To\YourProject\Saved\Logs\UE5.log",
    [int]$MaxSizeMB = 1024,
    [int]$MaxFiles = 5
)

# Check log size
$logFile = Get-Item $LogPath
if ($logFile.Length -lt $MaxSizeMB * 1MB) { exit 0 }

# Find running Unreal Engine process
$process = Get-Process -Name "UE5Editor" -ErrorAction SilentlyContinue
if (-not $process) { $process = Get-Process -Name "YourProject" -ErrorAction SilentlyContinue }
if (-not $process) { exit 1 }

# Step 1: Trigger Unreal to re-open logs by toggling verbosity (requires Engine.ini modification)
# This forces a log file handle release and reacquisition
# Add to Engine.ini: [Core.Log] LogLivelink=Verbose
# The rotation script sends a console command via UDP or named pipe to toggle logging
# For this example, we assume you've set up a simple UDP listener in the engine

# Step 2: Rename the locked file (works after handle is released)
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$rotatedPath = "$LogPath.$timestamp"
Move-Item $LogPath $rotatedPath -Force -ErrorAction SilentlyContinue

# Step 3: Cleanup old files
Get-ChildItem "$LogPath.*" | Sort-Object LastWriteTime -Descending | Select-Object -Skip $MaxFiles | Remove-Item -Force

# Step 4: Compress the rotated log (optional)
Compress-Archive -Path $rotatedPath -DestinationPath "$rotatedPath.zip" -Force
Remove-Item $rotatedPath -Force

Unreal Engine Integration Code (for native rotation):
Add this to your GameInstance or engine module to implement rotation at write time.

// In your GameInstance.cpp or custom Engine module
void UMyGameInstance::Init()
{
    // Override log handler
    FOutputDevice* OriginalLog = GLog;
    GLog = new FMyRotatingLogDevice(OriginalLog);
}

class FMyRotatingLogDevice : public FOutputDevice
{
public:
    FMyRotatingLogDevice(FOutputDevice* InInner) : Inner(InInner) {}

    virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override
    {
        // Write to inner device
        Inner->Serialize(V, Verbosity, Category);

        // Check file size (simplified)
        static uint64 WriteCount = 0;
        if (++WriteCount > 10000) // Sample every 10k messages
        {
            FString LogPath = FPaths::ProjectSavedDir() / "Logs" / "UE5.log";
            if (FPlatformFileManager::Get().GetPlatformFile().FileSize(*LogPath) > 500 * 1024 * 1024)
            {
                Rotate();
                WriteCount = 0;
            }
        }
    }

    void Rotate()
    {
        // Close current handle by flushing and reinitializing
        if (Inner->IsMemoryOnly()) return;

        FString OldPath = FPaths::ProjectSavedDir() / "Logs" / "UE5.log";
        FString NewPath = OldPath + "." + FDateTime::Now().ToString();

        // Move requires the file to be closed. We trigger this by:
        // 1. Flushing
        // 2. Creating a new output device instance
        // 3. The old device's destructor closes the handle

        FOutputDevice* NewDevice = new FOutputDeviceFile(*FPaths::ProjectSavedDir() / "Logs");
        GLog = NewDevice;
        delete Inner; // Closes handle
        Inner = NewDevice;

        // Move rotated file
        FPlatformFileManager::Get().GetPlatformFile().MoveFile(*NewPath, *OldPath);
    }

private:
    FOutputDevice* Inner;
};

How Senior Engineers Fix It

Senior engineers implement defense-in-depth strategies that acknowledge Windows limitations and Unreal’s architecture:

  • Application-Level Rotation: Subclass FOutputDevice in C++ to intercept log writes, track size, and automatically rotate without external dependencies
  • Process-Managed Restarts: Deploy Unreal as a Windows Service using NSSM (Non-Sucking Service Manager) or WinSW with built-in log size monitoring and automatic restart on threshold breach
  • Dual-Logging Architecture: Configure Unreal to write to a local rotating file (via custom FOutputDevice) and forward logs via UDP/TCP to a central syslog daemon that handles rotation centrally
  • Verbosity-Based Filtering: Reduce log volume at source by setting [Core.Log] LogLivelink=Warning instead of Verbose, cutting growth by 90%
  • External Tooling with Handle Management: Use handle.exe from Sysinternals to close the log file handle, then rotate, then reopen—scripted as a scheduled task
  • Disk Quota Enforcement: Set Windows NTFS quotas on the log directory to trigger alerts before exhaustion

Why Juniors Miss It

Junior engineers often approach this with Unix-centric thinking or incomplete understanding of Windows process isolation:

  • Assume Unix Parity: They search for logrotate and expect it to work natively on Windows, missing that it requires Cygwin or WSL which may not be available or appropriate
  • Ignore File Locking: They try to rename/delete the log while the process is running and get “Access Denied,” not understanding Windows file handle semantics
  • Over-rely on Configuration: They modify Engine.ini expecting a rotation setting that doesn’t exist, not realizing rotation requires code changes
  • Fear of Engine Modification: They avoid touching engine code, preferring external scripts, but those scripts fail because they don’t handle the handle release/reacquisition cycle properly
  • Misunderstand Log Buffering: They think logs are line-buffered and can be safely moved, not knowing Unreal’s async buffering requires explicit flush before rotation
  • No Monitoring: They implement rotation but lack disk space monitoring to verify it works, leading to false confidence and eventual failure