← All posts

How the logging SDK discovers every logger without you listing them

Integrating a logging management system usually starts with a registration chore: for every logger in your application, a line that says “this one is managed.” Forty loggers across ten libraries means forty lines of boilerplate, and any logger you forget is invisible to the system — and the forgotten ones are always the ones you end up needing.

The smplkit Logging SDK takes a different approach: call client.logging.install() once, and every logger your application uses is discovered automatically. No registration, no enumeration. This post covers how that works, the adapter architecture underneath it, and what happens to everything the SDK finds.

Explicit start(), not lazy init

The Flags and Config SDKs initialize lazily on first use. The Logging SDK requires an explicit call:

# Explicit start — logging framework is patched here
client.logging.start()

# Or the alias, which reads more naturally:
client.logging.install()

The difference is the side effect. Lazy initialization of the Flags SDK quietly opens a connection and fetches some state — benign. Initializing the Logging SDK monkey-patches your logging framework. This is not something you want happening implicitly, at first flag evaluation, in some distant code path. Patching the framework is a decision; the API forces you to make it at startup, in a predictable place.

The adapter interface

The SDK doesn’t implement discovery directly — it delegates to an adapter:

class LoggingAdapter:
    def discover(self) -> list[LoggerInfo]:
        """Return all loggers currently in the framework."""

    def apply_level(self, logger_name: str, level: str) -> None:
        """Set a logger to the specified level."""

    def get_level(self, logger_name: str) -> str:
        """Get the current level of a logger."""

    def install_hook(self, callback: Callable[[str], None]) -> None:
        """Start intercepting logger creation to discover new loggers."""

    def uninstall_hook(self) -> None:
        """Remove the interception."""

Python ships two implementations: StdlibAdapter (built-in logging, which transitively covers Django, Flask, and anything else on stdlib logging) and LoguruAdapter. At install() time the SDK auto-detects what’s present — if loguru imports, its adapter activates; if not, it’s skipped silently, with no warning nagging a project about a framework it doesn’t use. Both can be active at once.

How auto-discovery works

install_hook() is where the magic happens. For stdlib, it replaces logging.getLogger with a wrapped version:

original_get_logger = logging.getLogger

def hooked_get_logger(name=None):
    logger = original_get_logger(name)
    # Report this logger to the callback if we haven't seen it before
    if name and name not in self._seen:
        self._seen.add(name)
        callback(name)
    return logger

Every logging.getLogger("myapp.database") in the process — your code, a library, a framework — goes through the hook. Yes, that means we patch your logging framework, and if that sentence raises your eyebrows, good: it should be a deliberate choice, which is exactly why installation is explicit. The defense is that there’s no other seat in the house — the loggers you most need to discover are the ones buried in dependencies you didn’t write, and the only place they all pass through is the function that creates them.

discover() handles the other half: at install time it reads logging.Logger.manager.loggerDict, the registry Python’s logging framework already keeps, catching every logger created before the hook existed. Snapshot plus hook, and no logger escapes.

Telling the platform what was found

Discovery feeds a single bulk registration request at startup: the environment name, the service name, and everything the SDK found — discovered loggers, and for the other SDKs, the flag and config keys the application touched. The endpoint is idempotent, which matters more than it sounds: a service running on six containers sends six identical registrations, and the platform lands in the same state as if it had received one. No coordination needed, and no argument about which container owns the list. The alternative — asking developers to enumerate their loggers and environments up front — is ceremony, not value. Auto-discovery reports the loggers and environments that actually exist at runtime.

Environments get the same treatment. Connect with SMPLKIT_ENVIRONMENT=dev-mike and the platform creates the environment automatically, flagged AD_HOC. Explicitly created environments are STANDARD and appear in the main console list; the AD_HOC ones sit in their own group under a banner suggesting you promote or remove them, which is how a month of local development doesn’t quietly become permanent infrastructure.

Resources are identified by key, not UUID — the flag checkout-v2 lives at /api/v1/flags/checkout-v2 — so the SDK can report and address resources without ever tracking UUIDs. And registration only ever creates: a flag that stops being evaluated stays on the account, because the platform can’t tell a completed experiment from a kill switch that’s lying dormant on purpose. Deleting is human judgment; the console’s discovered-vs-created classification tells the human where to look.

Two SDK models

Like Flags and Config, Logging has a runtime model (the live state of loggers in a running process: install(), uninstall(), managed_loggers()) and a management model (the resources: which loggers are managed, their groups, their base levels).

mgmt = client.manage

# Get all loggers for the account
loggers = mgmt.logging.list()

# Mark a discovered logger as managed and set its level
logger = mgmt.logging.get("myapp.database")
logger.managed = True
logger.base_level = "DEBUG"
logger.save()

The management model is the one automation uses — setting up logging configuration in a deployment pipeline, before the application is even running.

Level application and the managed flag

When the platform broadcasts a level change, the SDK calls adapter.apply_level(...) — but only for loggers marked managed. A merely discovered logger keeps whatever level the application’s own configuration gave it; the SDK watches and reports, but doesn’t touch. Mark a logger managed and the SDK takes control of its level; unmark it and the application’s configuration takes over again. That handoff is what lets uninstall() return the process to exactly its pre-SDK state, with no residual overrides haunting anyone’s production.

Cross-language considerations

The adapter interface is identical in all six languages; the shipped adapters differ:

LanguageBuilt-in Adapters
Pythonstdlib, Loguru
TypeScriptWinston, Pino
Golog/slog, Uber Zap
JavaJUL, SLF4J + Logback, Log4j2
C#MEL (Microsoft.Extensions.Logging), Serilog
Rubystdlib Logger, SemanticLogger

One of these rows is longer than the others. The Java SDK ships three adapters because the Java logging ecosystem is fragmented in a way no other language manages: JUL is built in, SLF4J is the abstraction everyone codes against, and Logback and Log4j2 are the two dominant implementations underneath it, all with mutually incompatible APIs. Three adapters is what peace looks like there.

The custom-adapter path is documented and tested in every language: implement the five methods, call client.logging.register_adapter(MyAdapter()), and your in-house logging wrapper is a first-class citizen.

Still on the list

Level changes apply, but the SDK doesn’t sample any actual log output — forwarding a sample to the platform would turn “remote level control” into “remote level control plus log aggregation,” which is a real scope decision rather than a feature gap. The console shows the logger hierarchy but doesn’t yet preview what a parent-level change will cascade onto. And there’s no conditional application (“set DEBUG only if currently INFO”) for automation that wants to be careful in one direction only. None of them is off the table; nobody has needed them yet.