Quick answer: Set bUseLoggingInShipping = true in your Target.cs, register a custom FOutputDevice that buffers the last 500 log lines in a ring, and attach that buffer to bug reports. This gives you post-mortem logs from shipping players without the bloat of a full log file or the privacy concerns of writing personally identifiable data to disk.

One of the most frustrating moments in Unreal debugging is seeing a player report “the game crashed” with nothing else — no log file, no stack trace, just vibes. By default, Unreal strips logging in Shipping builds, which means your carefully placed UE_LOG calls simply don’t produce any output for your actual players. Here’s how to fix that without shipping a Development build.

Why Logging Is Stripped in Shipping

Unreal defines NO_LOGGING=1 in Shipping configurations by default. This causes the UE_LOG macro to expand to a no-op for most verbosity levels. The engine does this to reduce binary size, minimize string constants in memory, and eliminate any chance of log output slowing down the release build. It’s a reasonable default, but it leaves you blind when players report problems.

The good news is that this is fully configurable. Unreal provides a target property, bUseLoggingInShipping, that re-enables log output in Shipping configurations. Setting it to true adds a small amount of binary size and a negligible runtime cost for typical log volumes. Unless you are logging thousands of lines per frame, you won’t notice the performance impact.

// Source/YourGame.Target.cs
using UnrealBuildTool;
using System.Collections.Generic;

public class YourGameTarget : TargetRules
{
    public YourGameTarget(TargetInfo Target) : base(Target)
    {
        Type = TargetType.Game;
        DefaultBuildSettings = BuildSettingsVersion.V5;
        IncludeOrderVersion = EngineIncludeOrderVersion.Latest;

        ExtraModuleNames.Add("YourGame");

        // Keep UE_LOG output alive in Shipping builds
        bUseLoggingInShipping = true;

        // Optionally keep the console (requires both flags)
        bUseChecksInShipping = false;
    }
}

After changing the target file you must do a full rebuild — incremental rebuilds will not pick up the new logging flag. If you are using Unreal Build Accelerator or distributed builds, trigger a clean of the generated code and rebuild the project from scratch.

Hooking an FOutputDevice

Even with logging enabled in Shipping, you don’t want to read the entire log file from disk every time a player submits a bug report. The file can be megabytes large, and on mobile platforms you may not even have filesystem access at that path. A better approach is to buffer the most recent N log lines in memory using a custom FOutputDevice.

// FBugnetLogCapture.h
#pragma once

#include "CoreMinimal.h"
#include "Misc/OutputDevice.h"
#include "Containers/CircularBuffer.h"

class FBugnetLogCapture : public FOutputDevice
{
public:
    FBugnetLogCapture(int32 InMaxLines = 500);
    virtual ~FBugnetLogCapture();

    virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity,
                           const class FName& Category) override;

    FString GetSnapshot() const;
    void Clear();

private:
    mutable FCriticalSection BufferLock;
    TArray<FString> Buffer;
    int32 MaxLines;
    int32 WriteIndex;
};
// FBugnetLogCapture.cpp
#include "FBugnetLogCapture.h"

FBugnetLogCapture::FBugnetLogCapture(int32 InMaxLines)
    : MaxLines(InMaxLines), WriteIndex(0)
{
    Buffer.SetNum(MaxLines);
    if (GLog)
    {
        GLog->AddOutputDevice(this);
    }
}

FBugnetLogCapture::~FBugnetLogCapture()
{
    if (GLog)
    {
        GLog->RemoveOutputDevice(this);
    }
}

void FBugnetLogCapture::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity,
                                   const FName& Category)
{
    FScopeLock Lock(&BufferLock);
    Buffer[WriteIndex % MaxLines] = FString::Printf(
        TEXT("[%s][%s] %s"),
        *Category.ToString(),
        ToString(Verbosity),
        V);
    WriteIndex++;
}

FString FBugnetLogCapture::GetSnapshot() const
{
    FScopeLock Lock(&BufferLock);
    FString Out;
    int32 Start = FMath::Max(0, WriteIndex - MaxLines);
    for (int32 i = Start; i < WriteIndex; i++)
    {
        Out += Buffer[i % MaxLines] + TEXT("\n");
    }
    return Out;
}

void FBugnetLogCapture::Clear()
{
    FScopeLock Lock(&BufferLock);
    WriteIndex = 0;
    for (FString& Line : Buffer) Line.Reset();
}

Create an instance of this class in your GameInstance’s Init() and hold it as a member. It lives as long as the game runs and captures every log line via the GLog output device system. Thread safety is important because Unreal logs from multiple threads, which is why we use an FCriticalSection.

Attaching Logs to Bug Reports

When the player submits a bug report, call GetSnapshot() on your capture instance and pass the resulting string to the Bugnet SDK as an attachment or metadata field. Attachments are better for logs longer than a few KB because they don’t inflate the main report payload.

// In your bug report submission handler
void UBugReportWidget::SubmitReport(const FString& Description)
{
    UBugnetSubsystem* Bugnet = GEngine->GetEngineSubsystem<UBugnetSubsystem>();
    if (!Bugnet) return;

    FBugnetReport Report;
    Report.Title = FString::Printf(TEXT("Player report: %s"),
                                    *Description.Left(40));
    Report.Description = Description;

    // Attach the captured log snapshot
    if (LogCapture.IsValid())
    {
        Report.Attachments.Add(FBugnetAttachment{
            TEXT("console.log"),
            LogCapture->GetSnapshot(),
            TEXT("text/plain")
        });
    }

    Bugnet->SubmitReport(Report, FOnBugnetSubmitComplete::CreateLambda(
        [this](bool bSuccess) {
            if (bSuccess && LogCapture.IsValid())
            {
                LogCapture->Clear();
            }
        }));
}

Keep Personal Data Out of Logs

Before shipping, grep your codebase for any UE_LOG calls that output player names, email addresses, save file contents, or session tokens. Once logs start flowing back to your bug tracker, anything you log becomes data you’re storing — and you don’t want GDPR liability for a stack trace. Replace sensitive values with placeholders like [REDACTED] or strip them in a filter inside your FOutputDevice::Serialize override.

Similarly, avoid logging full file paths. Player home directories can contain identifying info. Log only the leaf filename or a hash of the path. And never log API keys, even in verbose development builds — a single accidental screenshot of a dev console can leak them.

Verifying the Capture Works

Package a Shipping build and run it. Generate some log output from gameplay, then trigger a bug report. Download the report from your Bugnet dashboard and confirm the log attachment is present and contains the expected lines. If the attachment is empty, check that bUseLoggingInShipping was actually applied — the most common cause is a stale Intermediate directory that didn’t pick up the target flag change.

Also test log volume. Play for 30 minutes, then submit a report. The attachment should contain at most 500 lines (your buffer size), showing the most recent logs. If the report contains zero lines after a 30-minute play session, your FOutputDevice isn’t being called — check that you added it to GLog and not a local OutputDeviceError instance.

Related Issues

If your Shipping build crashes on startup before logging initializes, see Fix: Unreal Packaged Build Crash on Startup. For general crash log analysis in Unreal, see Unreal Engine Crash Log Analysis. If you need help reading the stack traces that accompany your logs, check Reading Game Stack Traces: A Beginner’s Guide.

Logs without context are guesses. Logs with a bug report attached are fixes.