Skip to content

Logging

Everything logged by the engine, the editor, and your scripts goes through one logger. It writes to three places at once:

  • the console attached to the process,
  • Lumina.log, next to the executable (rotated per run, five kept),
  • the editor’s Output Log panel (Ctrl+J).

Logging is asynchronous: the calling thread formats the message and hands it off, and a background thread does the writing. It’s cheap enough to call from gameplay code, and it’s safe from any thread.

Include Log/Log.h (already in the precompiled header for most modules) and use the macros. The format string is std::format syntax.

LOG_INFO("Loaded {} assets in {:.2f} ms", Assets.size(), ElapsedMs);
LOG_WARN("Mesh '{}' has no material slot {}", Mesh->GetName(), SlotIndex);
LOG_ERROR("Failed to open {}", Path);

Arguments are checked at compile time, so a mismatched placeholder is a build error, not a garbled line at runtime.

MacroSeveritySurvives Shipping
LOG_CRITICALcriticalyes
LOG_ERRORerroryes
LOG_WARNwarningyes
LOG_DISPLAYinfoyes
LOG_INFOinfono
LOG_DEBUGdebugno
LOG_TRACEtraceno

LOG_TRACE / LOG_DEBUG / LOG_INFO compile to nothing in Shipping builds (they’re gated on the VerboseLogging build feature), so their arguments cost nothing there. Use them freely for everyday status.

LOG_DISPLAY is the one info-severity macro that survives Shipping. Reserve it for one-shot boot and system milestones you’d want in a packaged game’s log, like mounted PAKs, “loading startup map X”, or a linked plugin module. It is not a general info channel; frequent use drowns out the signal.

A single argument is logged verbatim, so braces in it are harmless:

LOG_ERROR(ErrorText); // ErrorText is an FString / const char* / FStringView

FString, FStringView, FName, FGuid, and FTransform already have a std::formatter, so they can be passed straight in. Math types don’t, so use Math::ToString:

LOG_INFO("Spawned {} at {}", Prefab->GetName(), Math::ToString(Location));

For your own types, specialize std::formatter the same way the engine ones do.

Log writes reach the OS at the end of every batch, so a crash won’t lose them. If you need a hard guarantee before doing something dangerous, call Logging::Flush(), which blocks until everything logged so far has landed. The crash handler, the assert handler, and the hang watchdog already do this.

Use the static Debug class. It’s available anywhere in script code.

public override void OnReady()
{
Debug.Log($"{Entity} ready with {Health} hp");
Debug.LogWarning("No spawn point assigned, falling back to origin");
Debug.LogError("Target prefab failed to load");
}
CallSeverity
Debug.Loginfo
Debug.LogWarningwarning
Debug.LogErrorerror

Use C# string interpolation ($"..."), the message is a plain string by the time it reaches the engine.

Script messages are tagged [C#] in the log, so they’re easy to pick out:

[2026-07-28 14:51:22.147] [info ] [9128] [C#] Loaded C# scripts [generation 1]

Unhandled exceptions in script callbacks are logged as errors automatically, with the stack trace, so you don’t need to wrap callbacks in try/catch just to see them.

The file log carries the full detail; the console is the short form.

[2026-07-28 14:51:18.682] [info ] [9128] Job system online: 30 workers
└─ date + time └─ severity └─ thread id

The thread id matters more than it looks: the engine runs gameplay, rendering, and job workers concurrently, so two interleaved lines are often two threads, not one confused system.

In the editor, the Output Log panel filters by severity and text, and doubles as the console-variable command line.