-
Notifications
You must be signed in to change notification settings - Fork 0
Log System
"Every log line in your project carries domain-specific meaning. An audio event has a channel, a volume, and a backend. A save operation has a slot, a domain, and a byte count. Treating them all as flat text is not logging — it is data loss."
Unreal Engine's built-in logging is a flat text stream. Every category, every system, every domain writes the same columns: timestamp, verbosity, category name, and a free-form string message.
[2026.02.23-14:30:01] Log LogAudio: Playing sound FX_Explosion on Channel 3
[2026.02.23-14:30:01] Log LogSave: Saving slot Autosave_001, domain Player, 24KB
[2026.02.23-14:30:02] Log LogFlow: State changed Global: MainMenu -> InGame
[2026.02.23-14:30:02] Log LogPSO: Pipeline 0x7FA3 compiled in 12ms (Context: Gameplay)
Four log lines. Four completely different domains. Four different sets of meaningful data. Yet they all render identically: a timestamp and a string.
When you are debugging an audio issue, you want to see sound names, channel assignments, volumes, and which backend is processing the sound. When you are debugging a save system issue, you want to see slot names, data domains, byte sizes, and operation types. The flat text format forces you to grep through strings, mentally parse the data, and reconstruct the structured information that was destroyed when it was serialized to a log line.
This has three consequences:
Domain blindness. A log viewer that treats all categories equally gives you no advantage over a text file. You cannot sort audio logs by channel. You cannot filter save logs by domain. You cannot graph PSO compilation times. The structure is gone.
Extension fragility. When a new system is added, the log viewer does not know about it. Someone has to manually add parsing logic, custom columns, and display code. The viewer becomes tightly coupled to every system it knows about.
Context switching cost. During a debugging session, you constantly switch between audio logs, save logs, and game flow logs. Each switch requires mental recalibration because the visual presentation is identical. There are no domain-specific cues — no color coding, no specialized columns, no contextual detail views.
The root problem is that traditional logging discards structure at write time and never recovers it.
PGX Log v3.0 is a polymorphic observability system. Instead of flat text, every log entry carries typed key-value parameters. Instead of a single viewer, each PGX domain has its own renderer that knows how to visualize its entries.
Every log entry contains:
- Standard fields: message, category, verbosity, timestamp, frame number, session ID
- A domain tag that identifies which system produced it
- An array of typed parameters (key-value pairs with explicit data types)
- Optional gameplay tags for cross-cutting concerns
When the Audio system logs a sound event, it does not format a string. It writes an entry with parameters: SoundEvent = "FX_Explosion", Channel = 3, Volume = 0.8, Backend = "Wwise". The data remains structured from write time through storage through display.
Each PGX system has a dedicated renderer — a display component that knows how to turn that system's log entries into meaningful visualizations.
Log Entry (structured)
|
+-- DomainTag: "PGX.Audio"
+-- Params: [SoundEvent, Channel, Volume, Backend]
|
v
Renderer Registry
|
+-- "PGX.Audio" -> Audio Renderer
+-- "PGX.Save" -> Save Renderer
+-- "PGX.GameFlow" -> GameFlow Renderer
+-- (unknown) -> Generic Renderer
|
v
Audio Renderer
|
+-- Row: [Time] [Sound Event] [Channel] [Volume] [Backend]
+-- Detail: Waveform info, channel state, fallback chain
The Log Viewer does not contain any system-specific code. It asks the registry "give me the renderer for this domain tag" and delegates all display logic to that renderer. This means:
- Adding a new domain requires zero changes to the viewer
- Each domain's visualization is self-contained
- The core system has no compile-time dependencies on any domain
| Domain | Specialized Columns |
|---|---|
| Audio | Sound event, channel, volume, backend |
| Save | Slot name, data domain, byte size, operation type |
| GameFlow | Source state, destination state, trigger, phase |
| PSO | Pipeline ID, compilation status, duration, context |
| LevelFlow | Level name, action, sub-level, reason |
| Loading | Loading phase, progress percentage, context, duration |
| Profile | Platform, capability name, budget name, value |
| MGOS | Pool name, allocation count, memory used, delta |
| Data Registry | Database name, item tag, action, version |
| Construction | Slot type, resolved class, source mode |
| Message | Channel, payload type, sender, size |
| EventHandler | Event tag, resolution result, handler name, exec time |
| Generic | Standard columns (time, verbosity, category, message) |
The Generic renderer is the fallback. Any log entry without a recognized domain tag, or from a system that has not registered a custom renderer, uses the Generic renderer. It displays the standard flat-text format — backward compatible with the mental model every Unreal developer already has.
+------------------------------------------------------------------+
| LOG SUBSYSTEM |
| (GameInstanceSubsystem) |
| |
| +------------------+ +-------------------+ +----------------+ |
| | Ring Buffer | | Domain Registry | | Session Mgr | |
| | | | | | | |
| | Entry array[] | | Tag -> Config DA | | Session ID | |
| | Configurable | | Tag -> Renderer | | Start/End time | |
| | max capacity | | Auto-discovered | | Entry counts | |
| +------------------+ +-------------------+ +----------------+ |
| |
| +------------------+ +-------------------+ +----------------+ |
| | Filter Engine | | Export Engine | | Delegates | |
| | | | | | | |
| | Global min verb | | Async JSON export | | EntryAdded | |
| | Per-category | | Session metadata | | SessionUpdated | |
| | Domain filter | | Filtered subsets | | DomainRegd | |
| +------------------+ +-------------------+ +----------------+ |
+------------------------------------------------------------------+
|
+--------------------+--------------------+
| | |
+--------v-------+ +--------v-------+ +---------v------+
| Domain Config | | Domain Config | | Domain Config |
| DA: Audio | | DA: Save | | DA: GameFlow |
| | | | | |
| Tag | | Tag | | Tag |
| Color | | Color | | Color |
| Display Name | | Display Name | | Display Name |
| Renderer ref | | Renderer ref | | Renderer ref |
+----------------+ +----------------+ +----------------+
| | |
+--------v-------+ +--------v-------+ +---------v------+
| Audio Renderer | | Save Renderer | | Flow Renderer |
| | | | | |
| BuildRowWidget | | BuildRowWidget | | BuildRowWidget |
| BuildDetail | | BuildDetail | | BuildDetail |
| GetColumns | | GetColumns | | GetColumns |
+----------------+ +----------------+ +----------------+
System writes entry Viewer requests display
================== =========================
Audio System Log Viewer Tab
| |
v v
AddEntry( GetEntriesForDomain("PGX.Audio")
Message, |
DomainTag, v
Params[ Registry.GetRenderer("PGX.Audio")
SoundEvent, |
Channel, v
Volume, AudioRenderer.BuildRowWidget(Entry)
Backend |
] v
) +------+--------+------+---------+
| | Time | Sound | Ch | Volume |
v +------+--------+------+---------+
Ring Buffer | 14:30| Explo..| 3 | 0.80 |
(stores entry) +------+--------+------+---------+
|
v
OnEntryAdded delegate
(notifies viewer)
Adding a new domain to the Log system requires three things:
1. Define your parameters
Write entries with typed key-value Params
(no base class changes, no registration code)
2. Create a renderer
Inherit from the base renderer
Override: row widget, detail widget, column definitions
(Blueprint-subclassable for rapid iteration)
3. Create a Domain Config asset
Set the domain tag, display color, name, renderer reference
(AssetRegistry auto-discovers it — no code registration)
Result: Log Viewer shows your domain with custom columns
Zero changes to the viewer or core log system
This pattern means the Log system can support domains that did not exist when it was written. A game-specific "Combat" domain, a "Networking" domain, a "Quest" domain — each gets its own visualization without touching framework code.
- Ring buffer: Fixed-capacity circular buffer. When full, oldest entries are evicted. Capacity is configurable via the Config Data Asset. This prevents unbounded memory growth during long sessions.
- Session tracking: Each play session gets a unique ID, start time, and end time. Entry and warning/error counts are tracked per session.
- Global minimum verbosity: Set a floor — entries below this level are discarded at write time.
- Per-category verbosity: Override the minimum for specific categories (e.g., show Debug for Audio but only Warning for everything else).
- Domain filter: In the viewer, filter by domain tag to show only entries from specific systems.
- Text search: Full-text search across message content and parameter values.
- Tag filter: Filter entries by gameplay tags for cross-cutting concerns.
- Async JSON export: Export the current session's entries (or a filtered subset) to a JSON file. The export runs asynchronously to avoid blocking the editor.
- Session metadata: Exported files include session information, entry counts, and filter state at the time of export.
- Live mode: The viewer auto-refreshes as new entries arrive. Entries stream in real time.
- Manual mode: The viewer shows a snapshot. New entries are buffered but not displayed until the developer explicitly refreshes. Useful when inspecting specific entries without the list scrolling away.
Each log entry can be expanded into a detail window. The detail window uses the domain's renderer to show the full structured data — all parameters, all tags, the complete context of that entry.
Detail windows display a STALE/LIVE indicator. If the detail window was opened from a Live viewer, it shows LIVE. If the session has ended (PIE stopped), it shows STALE to prevent confusion about whether the data is current.
The Log Config asset controls system-wide behavior:
| Property | Description |
|---|---|
| Maximum entries | Ring buffer capacity |
| Default verbosity | Global minimum verbosity level |
| Screen print | Enable/disable on-screen log overlay |
| Per-category overrides | Map of category names to verbosity levels |
| Export path | Default directory for JSON exports |
Each domain's appearance and behavior is controlled by a separate Data Asset:
| Property | Description |
|---|---|
| Domain tag | The gameplay tag that identifies this domain |
| Display color | Color used in the viewer for this domain |
| Display name | Human-readable name shown in filter dropdowns |
| Renderer source mode | Default renderer, custom class, or Blueprint class |
| Renderer class reference | The renderer to use for this domain |
| Auto-infer categories | Automatically map log categories to this domain |
For projects with many domains, a DataTable can define domain configurations in bulk. Individual Data Assets take priority over DataTable rows for the same domain tag.
+------------------------------------------------------------------+
| PGX Log Viewer [Live|Manual] [Pin] |
+------------------------------------------------------------------+
| Domain: [All v] Verbosity: [Info v] Search: [____________] |
+------------------------------------------------------------------+
| [Audio] [Save] [GameFlow] [PSO] [...] [Generic] <- filters |
+------------------------------------------------------------------+
| |
| AUDIO DOMAIN VIEW (type-adaptive columns) |
| +------+--------+----------+------+---------+ |
| | Time | Sound | Channel | Vol | Backend | |
| +------+--------+----------+------+---------+ |
| | 14:30| Explo..| 3 | 0.80 | Default | |
| | 14:30| Music | 0 | 1.00 | Default | |
| | 14:31| Dialog | 1 | 0.95 | Default | |
| +------+--------+----------+------+---------+ |
| |
| SAVE DOMAIN VIEW (different columns, same viewer) |
| +------+--------+----------+----------+---------+ |
| | Time | Slot | Domain | Size | Op | |
| +------+--------+----------+----------+---------+ |
| | 14:32| Auto_01| Player | 24.5 KB | Save | |
| | 14:33| Auto_01| World | 128.0 KB | Save | |
| +------+--------+----------+----------+---------+ |
| |
+------------------------------------------------------------------+
| Session: abc123 | Entries: 847 | Warnings: 12 | Errors: 0 |
+------------------------------------------------------------------+
Key interactions:
- Domain filter bar: Click domain buttons to show/hide entries from each system. Each button is colored with the domain's configured color.
- Live/Manual toggle: Switch between auto-refreshing and snapshot modes.
- Column adaptation: When switching domain filters, the columns change to match the selected domain's renderer.
- Click-to-detail: Click any row to open a detail window with the full structured data.
- JSON export: Export button generates an async JSON file with current filter state.
Writing domain-specific log entries requires no special setup. When your system writes a log entry, include the domain tag and typed parameters. The Log system handles routing, storage, and display.
The pattern is: write structured data at the source, display structured data at the destination, never lose structure in between.
Creating a custom domain renderer requires inheriting from the base renderer and overriding three methods:
- Column definitions: What columns your domain shows in the list view
- Row widget: How a single entry renders in the list view
- Detail widget: How a single entry renders in the expanded detail view
Renderers are Blueprint-subclassable. A technical artist or designer can create custom domain visualizations without writing any native code.
| Event | When | Use Case |
|---|---|---|
| Entry Added | New entry written | Viewer refresh, external consumers |
| Session Updated | Session metadata changes | Dashboard stats |
| Domain Registered | New domain config discovered | Dynamic filter bar update |
| Command | Description |
|---|---|
pgx.log.stats |
Print session statistics (entries, warnings, errors, session duration) |
pgx.log.list |
List recent entries with optional count parameter |
pgx.log.filter |
Set runtime verbosity filter |
pgx.log.export |
Trigger async JSON export of current session |
pgx.log.domains |
List all registered domains with entry counts |
pgx.log.clear |
Clear the ring buffer (current session entries) |
Logging is the most used debugging tool in game development. It is also the most underinvested. Every studio has logging. Almost no studio has observability — the ability to understand system behavior through structured, domain-aware visualization.
The difference between a log line that says "Saving slot Autosave_001, domain Player, 24KB" and a log viewer that shows a table with sortable columns for Slot, Domain, Size, and Operation is the difference between reading a paragraph and reading a spreadsheet. Both contain the same information. One lets you find what you need.
Polymorphic rendering means the investment compounds. Each new system that adds a domain renderer makes the Log Viewer more valuable for the entire project. The Audio team's renderer helps the Audio team. The Save team's renderer helps the Save team. But the Log Viewer — the single tool everyone uses — gets better for everyone with every addition.
The extension pattern ensures this investment is future-proof. A renderer written today for Audio will not need to change when a Combat system is added next month. The viewer will display Combat logs with Combat-specific columns, and the Audio renderer will not know or care.
This is what observability looks like in a game framework: not more text, but better structure. Not more features in the viewer, but more knowledge in the data.
- Development Preview
- Getting Started
- Release branch catalog
- Public Plugin Matrix
- Early Preview Plugins
- Known Issues
- Architecture Overview
- Plugin Topology
- Module Reference
- Configuration and Registry
- Data-Driven Design
- Profiles and Budgets
- Gameplay Tag Architecture
- Initialization Pipeline
- Cross-Plugin Communication
- Message System
- Event Handlers
- Logging and Trace
- Runtime Flows
- Blueprint API Design
- Editor Integration
- Editor Visual System