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:
- Wire the adapter for your logging framework (Python: stdlib
loggingis auto-loaded; Go/TS/Java/C# pick adapters explicitly). - Call
client.logging.install()once at startup. - 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.
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
@client.logging.on_change
def on_any_change(event):
print(f"{event.id}: level={event.level} (source={event.source})")ID-Scoped Listener
@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.
await client.logging.refresh()Level Resolution
When the SDK resolves a logger's effective level, it walks a chain (first non-null wins):
- Per-environment override on the logger (
production,staging, etc.) - Logger's base level
- Group level (if assigned)
- Dot-notation ancestor (
acme.payments.refundsfalls through toacme.payments, thenacme) - 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()andlogging.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 afterinstall()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 registeredILoggerProvider, so every category is observable. - Go —
slog/zapadapters wrap the logger you hand them; new loggers must be created via the adapter, or registered explicitly viaclient.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:
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:
| Method | Responsibility |
|---|---|
name | Human-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.
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:
[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
- Logging Management — Create and manage loggers programmatically
- Smpl Config — Manage application configuration across environments
- API Reference — Logging — Full REST API documentation

