Skip to content

Logging Runtime

The runtime API hooks smplkit into your application's logging framework. Server-managed levels are pushed onto your loggers in real time over WebSocket — no redeploy, no restart. Each language ships adapters for the dominant logging frameworks; new logger categories are auto-discovered as your code creates them.

The high-level shape is the same in every SDK:

  1. Wire the adapter for your logging framework (Python: stdlib logging is auto-loaded; Go/TS/Java/C# pick adapters explicitly).
  2. Call client.logging.install() once at startup.
  3. Use your logger normally. Server-pushed level changes apply automatically.

Install

install() discovers existing loggers, bulk-registers them with the platform, applies the resolved levels back onto the native loggers, and subscribes to the live-updates WebSocket. It's idempotent — safe to call multiple times.

python
import asyncio
from smplkit import AsyncSmplClient

async with AsyncSmplClient(environment="production", service="my-service") as client:
    await client.logging.install()
    print("All loggers are now controlled by smplkit")

The Python SDK auto-loads the stdlib logging adapter — every logger created via logging.getLogger(name) (including by third-party libraries) is discovered and managed. Loguru is also supported via from smplkit.logging.adapters.loguru_adapter import LoguruAdapter; client.logging.register_adapter(LoguruAdapter()) before install().

Change Listeners

Register callbacks that fire when a logger's level changes — either via WebSocket push or a manual refresh(). Two scoping levels: global (any logger) and id-scoped (specific logger).

Global Listener

python
@client.logging.on_change
def on_any_change(event):
    print(f"{event.id}: level={event.level} (source={event.source})")

ID-Scoped Listener

python
@client.logging.on_change("acme.payments")
def on_payments_change(event):
    print(f"acme.payments level changed to {event.level}")

Manual Refresh

refresh() re-fetches managed levels and re-applies them to the native loggers. WebSocket events trigger this automatically; call manually after suspecting drift, or in short-lived scripts where you don't want to wait on the WebSocket.

python
await client.logging.refresh()

Level Resolution

When the SDK resolves a logger's effective level, it walks a chain (first non-null wins):

  1. Per-environment override on the logger (production, staging, etc.)
  2. Logger's base level
  3. Group level (if assigned)
  4. Dot-notation ancestor (acme.payments.refunds falls through to acme.payments, then acme)
  5. System fallback: INFO

A change at any level of the chain re-resolves and re-applies the effective level to all dependent loggers — push the acme group from INFO to DEBUG and every dot-notation child without an explicit level shifts too.

Auto-Discovery

Loggers created after install() are auto-discovered and registered with the platform on a periodic flush (~5 s). The mechanism varies by language:

  • Python — monkey-patches logging.getLogger() and logging.Logger.setLevel() so new loggers (including from third-party libraries) are queued for registration as they're created.
  • TypeScript — winston named loggers and pino() / logger.child() calls made after install() are tracked. Pre-existing pino loggers must be recreated.
  • Java — JUL is polled; SLF4J–Logback and Log4j2 hook into framework events when they're on the classpath.
  • C# — every ILoggerFactory.CreateLogger(...) call routes through the registered ILoggerProvider, so every category is observable.
  • Goslog/zap adapters wrap the logger you hand them; new loggers must be created via the adapter, or registered explicitly via client.manage.loggers.register([...]).

To force the buffer to flush early (e.g. in a script that exits before the periodic tick), call client.manage.loggers.flush().

Sync Client (Python)

For synchronous applications (Django, Flask, CLI tools), use SmplClient instead of AsyncSmplClient:

python
from smplkit import SmplClient

with SmplClient(environment="production", service="my-service") as client:
    @client.logging.on_change("acme.payments")
    def on_change(event):
        print(f"acme.payments → {event.level}")

    client.logging.install()

Custom Adapters

Each SDK exposes a LoggingAdapter interface so you can add first-class support for any logging framework not covered by the built-ins.

Interface

All five methods must be implemented:

MethodResponsibility
nameHuman-readable identifier (e.g. "structlog")
discover()Return every existing logger as (name, explicit_level, effective_level)
apply_level(name, level)Set a logger's level from the server-resolved value
install_hook(callback)Intercept future logger creation; call callback(name, explicit, effective)
uninstall_hook()Tear down the interception

Registering an adapter

Register before install(). Registering any adapter disables auto-loading — only the adapters you register explicitly are used.

python
from smplkit.logging.adapters.base import LoggingAdapter

class StructlogAdapter(LoggingAdapter):
    @property
    def name(self) -> str:
        return "structlog"

    def discover(self): ...
    def apply_level(self, name, level): ...
    def install_hook(self, on_new_logger): ...
    def uninstall_hook(self): ...

client.logging.register_adapter(StructlogAdapter())
client.logging.install()

Packaging an adapter for auto-discovery

Adapters distributed as standalone packages can be wired in automatically without any caller code change.

Declare an entry point under the smplkit.logging.adapters group in your package's pyproject.toml:

toml
[project.entry-points."smplkit.logging.adapters"]
structlog = "my_package.adapter:StructlogAdapter"

The SDK discovers every registered entry point via importlib.metadata when install() runs.

Next Steps