Quick answer: Games should use at least four log levels: Debug for development-only verbose output, Info for routine events like scene loads and save operations, Warning for recoverable issues like missing assets with fallbacks, and Error for failures that affect gameplay or stability.

Following best practices for game error logging helps you catch and resolve issues faster. A player reports that the game crashes after ten minutes of play. You ask for steps to reproduce. They say “I was just playing normally.” Without logs, this report is a dead end. With structured error logging, you open the log file attached to their crash report and see exactly which function threw an exception, what the game state was at the time, and what happened in the thirty seconds before the crash. Good logging transforms mystery crashes into fixable bugs.

Structured Logging vs. Print Statements

The first step toward effective game logging is moving beyond print() and Debug.Log() scattered throughout the codebase. These produce unstructured output that is hard to filter, hard to parse, and impossible to search at scale. Structured logging means every log entry has a consistent format with machine-readable fields.

// C# structured logger for Unity
public enum LogLevel { Debug, Info, Warning, Error }

public class GameLogger
{
    private static LogLevel _minLevel = LogLevel.Info;
    private static StreamWriter _writer;
    private static readonly object _lock = new();

    public static void Init(string logPath, LogLevel minLevel)
    {
        _minLevel = minLevel;
        _writer = new StreamWriter(logPath, append: true);
        _writer.AutoFlush = false; // Manual flush for performance
    }

    public static void Log(LogLevel level, string system,
        string message, Dictionary<string, object> context = null)
    {
        if (level < _minLevel) return;

        var entry = new Dictionary<string, object>
        {
            ["timestamp"] = DateTime.UtcNow.ToString("o"),
            ["level"] = level.ToString(),
            ["system"] = system,
            ["message"] = message,
            ["frame"] = Time.frameCount
        };

        if (context != null)
            entry["context"] = context;

        var json = JsonSerializer.Serialize(entry);
        lock (_lock)
        {
            _writer.WriteLine(json);
        }
    }

    public static void Flush()
    {
        lock (_lock) { _writer.Flush(); }
    }
}

Each log entry is a JSON object with a timestamp, log level, source system name, message, and optional context dictionary. This format can be parsed by tools, filtered by level or system, and searched with standard JSON query languages. Compare this to a raw print statement like "Enemy spawned at position 42, 0, 17" — there is no way to programmatically distinguish that from a debug message or an error.

Choosing the Right Log Levels

Log levels control the verbosity of your output. In development, you want to see everything. In production, you want only the information that helps diagnose real problems. A four-level system works well for most games:

Debug: Verbose output for development. Physics tick details, AI decision trees, network packet contents. Never active in production. Use liberally during development and strip or disable before shipping.

Info: Routine events that mark the progression of a session. Scene loaded, save file written, player connected to server, achievement unlocked. These entries create a timeline of the session that provides context when reading error logs.

Warning: Something unexpected happened, but the game recovered. A missing texture was replaced with a fallback. A network request timed out and was retried. An animation clip was not found and was skipped. Warnings indicate problems that should be fixed but are not blocking the player.

Error: Something broke. A null reference was hit, a file failed to load with no fallback, a network connection was lost, or the game state became inconsistent. Errors are the entries your team investigates first when triaging bug reports.

# Usage examples in Godot
# Debug: only visible during development
GameLogger.log(LogLevel.DEBUG, "AI",
    "Enemy %s evaluating %d targets" % [enemy.name, targets.size()])

# Info: session timeline events
GameLogger.log(LogLevel.INFO, "SceneManager",
    "Loaded scene: %s in %.1fms" % [scene_name, load_time])

# Warning: recovered from an issue
GameLogger.log(LogLevel.WARNING, "AssetLoader",
    "Texture not found: %s, using fallback" % [texture_path])

# Error: something broke
GameLogger.log(LogLevel.ERROR, "SaveSystem",
    "Failed to write save file: %s" % [error_message],
    {"path": save_path, "bytes": data_size})

Performance Considerations

Logging in a game is different from logging in a web server because games have a frame budget. Every millisecond spent on logging is a millisecond not spent on rendering or simulation. There are three common performance pitfalls to avoid.

String formatting in hot loops. Even if the log level check prevents the string from being written, many languages still evaluate the format arguments before calling the log function. In C#, use conditional compilation or check the level before formatting. In GDScript, wrap verbose logs in an if check.

Synchronous disk writes. Writing to a file on every log call blocks the calling thread until the OS completes the write. Buffer log entries in memory and flush them periodically — once per frame, or once per second. Flush immediately on Error-level entries so you do not lose the most important data in a crash.

Excessive log volume. A physics system logging every collision every frame at 60 FPS generates thousands of entries per second. In development, this might be acceptable. In production, it will fill the player’s disk and degrade performance. Set the production log level to Info or Warning and ensure Debug-level logging is completely disabled in release builds.

Log Rotation and File Management

Without rotation, log files grow indefinitely. A player who runs your game for hundreds of hours will end up with a multi-gigabyte log file that is impractical to read, transmit, or search. Implement rotation based on file size, session boundaries, or both.

// Log rotation: start a new file each session, keep last 5
public static void RotateLogs(string logDir, int maxFiles = 5)
{
    var files = Directory.GetFiles(logDir, "game_*.log")
        .OrderByDescending(f => File.GetCreationTimeUtc(f))
        .ToList();

    // Delete oldest files beyond the limit
    for (int i = maxFiles - 1; i < files.Count; i++)
        File.Delete(files[i]);
}

public static string CreateSessionLog(string logDir)
{
    var timestamp = DateTime.UtcNow.ToString("yyyyMMdd_HHmmss");
    var path = Path.Combine(logDir, $"game_{timestamp}.log");
    RotateLogs(logDir);
    return path;
}

A common strategy is to create a new log file at the start of each game session and keep the last five session logs. When a crash report is submitted, attach the current session log plus the previous one (in case the crash happened at session start and the relevant context is in the prior session).

What to Log and What Not to Log

Log too little and you cannot diagnose bugs. Log too much and you drown in noise. The right balance depends on the system, but some guidelines apply universally.

Always log: application startup and shutdown, scene and level transitions, save and load operations (including success or failure), network connection and disconnection events, unhandled exceptions with full stack traces, resource loading failures, and any state change that affects gameplay.

Never log: player passwords or authentication tokens, payment information, personal data like email addresses or real names (hash these if you need identifiers), per-frame data in production (positions, velocities, input states), and the contents of encrypted or compressed data blobs.

Log conditionally: AI decision details (Debug level only), network packet contents (Debug level only), physics collision details (Debug level only), and performance metrics like frame time and memory usage (sample periodically rather than every frame).

“The goal of logging is not to record everything that happens. It is to record enough that when something goes wrong, you can reconstruct why.”

Adding Context to Error Logs

A log entry that says "NullReferenceException in CombatSystem.cs:142" tells you where the error occurred. Add context and it also tells you why: which enemy was being processed, what the player was doing, which quest was active, how long the session had been running. Context transforms errors from code locations into debugging narratives.

Build a context stack that tracks the current game state. When an error is logged, automatically include the top entries from the context stack. This avoids the need to manually add context parameters to every log call — the logger knows the current scene, active quest, and recent player actions because the game state manager keeps it informed.

Related Issues

For a guide on using player-submitted logs to debug issues, see how to use player logs to debug game issues. For mobile-specific logging considerations, check how to set up error logging for mobile games. To learn about reading stack traces when you receive crash reports, see our beginners guide to reading game stack traces.

Start every game session log with a header that records the game version, platform, OS, GPU, and available memory. When a player sends you a log file, that header tells you everything you need to reproduce their environment.