Skip to content

LoggerPro — Runtime/Interfaces

The seams other people's code is allowed to replace.

Ten interfaces, and the thing worth understanding about all of them is not what they do but what they cost: these are public API in a shipped product, and third parties implement them. A member added to any one of these breaks everybody who wrote an implementation against the previous version — silently at compile time, in their project, after an update they did not ask for.

That single constraint is why this folder looks the way it does, and three of its files exist only because of it.


What the constraint does to a design

Split a new capability into its own interface. ISessionRestorable is the one storage backends implement to have their contents handed back after a domain reload. It could have been a Restore method on ILoggerStorage. It is not, for two reasons: adding a member to a shipped interface breaks every existing implementation, and restoring is genuinely optional — a storage backed by a file or a database already survives a reload and has nothing to do.

Put the extra parameter on the concrete class. Captured Unity messages are queued and stored on a later tick, so stamping them on arrival dated a whole stalled burst to the moment the stall ended. The fix needs an emission time, and ILoggerStorage.AddLog cannot carry one. So InMemoryLoggerStorage has a second AddLog overload taking a DateTime, and the caller checks the type. The interface never moved.

Keep the old shape alive beside the new one. ILogExporter enumerated the destinations as methods — ExportToFile, ExportToJson, CopyToClipboard — which meant adding a format was a change to the interface, so the only people who could add one were us. ILogExportFormat replaced it. ILogExporter is still here, still honoured, still wired to ILoggerProContext.Exporter, and is not going anywhere.

Everything here follows from one rule: a member cannot be added to a shipped interface. When you need a shipped implementation to do more, the extra method goes on the concrete class and callers check the type.


The contracts

Interface Default implementation Assembly
ILoggerService LoggerService Runtime
ILoggerStorage InMemoryLoggerStorage Runtime
ISessionRestorable InMemoryLoggerStorage Runtime
ICategoryManager CategoryManager Runtime
ILogFormatter DefaultLogFormatter Runtime
ILoggerConfigProvider ProjectSettingsConfigProvider Runtime
ILoggerProContext LoggerProContextInstance Runtime
ILogExportFormat BuiltInExportFormats Editor
ILogExporter DefaultLogExporter Editor
IClearable several both

The two export interfaces are the exception to the folder's own location: they are declared in the runtime assembly because ILoggerProContext names them, but nothing implements them outside the editor. Writing a file is not something a player build does.


Logging

ILoggerService — the six severities, each taking a category, a message and an optional UnityEngine.Object context. This is what the static LoggerPro facade forwards to, and replacing it replaces what a log means. LoggerService is the shipped one; see Runtime/Shared/README.md for the order it does things in.

ILogFormatter — FormatHeader(category, type) and FormatDetails(message). Two strings in, two strings out, called on every log. Captured Unity messages skip it, because Unity's own trace is already attached and ours would be stapled on top.


Storage

ILoggerStorage — AddLog, GetLogs, Clear. The backing store for a session.

One obligation is not expressible in the signature and is stated in the XML docs instead: the optional context object is retained so the console can ping it, and an implementation must not persist that reference into an exported report. A report is a file sent to another person; a live UnityEngine.Object reference means nothing in their project and everything in yours.

ISessionRestorable — Restore(IReadOnlyList<LoggerProLogEntry>), for reinstating entries captured before a domain reload.

Restoring is not re-adding. AddLog stamps a fresh timestamp and counts as a new occurrence; these entries already happened, so their timestamps and duplicate counts have to survive intact. Anything already in the store was logged after the reload, so it is newer than everything being restored and has to stay after it.


Categories

ICategoryManager — IsEnabled, SetEnabled, EnsureRegistered, GetAll, RebuildMap.

EnsureRegistered runs on the logging hot path, for every log call, and the interface says so: an implementation must not perform a synchronous asset write for the common case where the category already exists. The shipped one makes it a single dictionary lookup.

Note what is absent. Removing a category is not here — it is LoggerPro.RemoveCategory, reaching a method on the concrete CategoryManager, for exactly the reason at the top of this file.


Configuration and services

ILoggerConfigProvider — one method, GetConfig(). Where the category list comes from.

Pass the provider, never the config it currently returns. "Reload From Disk" replaces that instance, and a CategoryManager holding the old one goes on showing the pre-reload list while every toggle and every newly registered category is written to an object nothing will save — the button looks like it did nothing and quietly costs the session's category changes.

ILoggerProContext — the six services above under one contract: Exporter, Formatter, Storage, Categories, ConfigProvider, Logger. This is what LoggerPro.Initialize(...) takes and what LoggerProRuntime.Context exposes, and it is the reason the test suite can run against an in-memory context instead of writing to a real project's configuration.

IClearable — void Clear(), and nothing else. Implemented by the scanner UIs, the warn-once set and the import session, so LoggerProClearRegistry.ClearAll can reset them together without knowing what any of them are.


Export

ILogExportFormat — Id, DisplayName, IsAvailable, and Export(IReadOnlyList<LoggerProLogEntry>). One way of getting logs out: a file format, the clipboard, a bug tracker, a build server.

The toolbar draws whatever is in the registry, in registration order, and LoggerPro's own five formats are registered through the same public call anyone else would use. There is no privileged built-in list.

  • Id is a stable identifier, and registering a second format with the same id replaces the first rather than duplicating it. That is what makes re-registering on every domain reload safe, and it also means you can override a built-in format by claiming its id. Namespace your own: "mystudio.jira", not "jira".
  • IsAvailable returning false hides the button. Use it when the format is switched off or not yet configured — a button that always fails is worse than no button.
  • The entries are passed in rather than read from the active context, so a format can be pointed at an imported report or a filtered subset, and can be tested without standing up a whole LoggerPro.
[InitializeOnLoad]
static class MyExporterInstaller
{
    static MyExporterInstaller() => LoggerProExportFormats.Register(new MyExporter());
}

ImportExport/Formats/README.md covers writing one.

ILogExporter — the legacy shape. Use ILogExportFormat for anything new.

Worth knowing why it was replaced rather than extended, because it is the clearest example of the cost: enumerating destinations as methods meant ClearScreenshots ended up on the export interface, obliging anyone writing a custom exporter to also implement deleting image files, which is not an export at all. That job now lives on LoggerProScreenshots, which matches only the files LoggerPro wrote and moves them to the trash rather than deleting every PNG in a folder the user chose. The method stays on the interface so existing implementations still compile; nothing in LoggerPro calls it.