Logging Runtime
The runtime API gives smplkit control over your application's log levels at runtime, without redeploying. For Python, the SDK integrates with Python's built-in logging module. For Java, the SDK integrates with java.util.logging. For other languages, the SDK provides change notification callbacks and level resolution.
Start the Runtime
start() is the explicit opt-in for runtime logging control. It is idempotent — calling it multiple times is safe. Management methods work without start().
What start() does varies by language:
- Python: Scans the logging registry for existing loggers, installs monkey-patches for continuous discovery, bulk-registers discovered loggers, fetches definitions, resolves and applies levels, opens WebSocket, starts periodic flush timer.
- TypeScript/Java/C#/Go: Wires the shared WebSocket for logger change events, marks the runtime as active.
await client.logging.start()Change Listeners
Register callbacks to react to logger changes. Two forms: global (fires for any logger change) and key-scoped (fires only for a specific logger key). Listeners can be registered before or after start() — registering before ensures no events are missed during initialization.
Global Listener
@client.logging.on_change
def on_any_change(event):
print(f"{event.key}: level={event.level}")Key-Scoped Listener
@client.logging.on_change("sqlalchemy.engine")
def on_sql_change(event):
print(f"sqlalchemy.engine level changed to {event.level}")Level Resolution Chain (Python)
When smplkit resolves a logger's effective level, it walks a chain (first non-null wins):
- Logger's own environment override
- Logger's own base level
- Group chain (recursive up the group hierarchy)
- Dot-notation ancestry (walk
app.payments→app) - System fallback:
INFO
This section is Python-specific because the Python SDK directly monkey-patches the logging module and applies resolved levels to Python loggers.
import logging as stdlib_logging
stdlib_logging.getLogger("app").setLevel(stdlib_logging.INFO)
stdlib_logging.getLogger("app.payments").setLevel(stdlib_logging.WARNING)
stdlib_logging.getLogger("sqlalchemy.engine").setLevel(stdlib_logging.WARNING)
await client.logging.start()After start(), smplkit resolves levels using the chain. For example, in the production environment:
- app → environment override
production=ERROR→ERROR - app.payments → no level, no group → ancestor
app→ERROR - sqlalchemy.engine → no level → group
databases→ groupproduction=WARN→WARN
The Python levels are updated in-process — no restart needed.
Dynamic Level Control
Change a level on the server (via Console UI or management API), and connected SDK instances receive the update via WebSocket. The Python SDK re-resolves and applies the new level to the Python logging framework automatically. Other languages receive the change event via their registered listeners.
Change a Group Level
When a group's level changes, all loggers assigned to that group shift.
db_group = await client.logging.management.get_group("databases")
db_group.setEnvironmentLevel("production", LogLevel.DEBUG)
await db_group.save()After the WebSocket delivers the update, sqlalchemy.engine (a member of the databases group) resolves to DEBUG instead of WARN.
Change an Ancestor Level
When an ancestor logger's level changes, dot-notation children that inherit from it shift.
app_lg = await client.logging.management.get("app")
app_lg.setEnvironmentLevel("production", LogLevel.TRACE)
await app_lg.save()After the update, app.payments (which inherits from app via dot-notation) resolves to TRACE.
Auto-Discovery (Python)
Python's SDK installs monkey-patches that intercept logging.getLogger() and logging.Logger.setLevel(). New loggers created after start() are automatically discovered and queued for bulk registration on the next periodic flush.
import logging
new_logger = logging.getLogger("app.notifications")
new_logger.setLevel(logging.INFO)The SDK intercepted getLogger() and queued app.notifications for registration. It will appear on the server after the next periodic flush (~5 seconds).
Logger Registration (Go)
Go doesn't have a global logger registry, so loggers must be registered explicitly before calling Start().
logging.RegisterLogger("com.acme.payments", smplkit.LogLevelInfo)
logging.RegisterLogger("com.acme.auth", smplkit.LogLevelDebug)
logging.RegisterLogger("com.acme.db", smplkit.LogLevelWarn)
err := logging.Start(ctx)Sync Client (Python)
For synchronous applications (Django, Flask, CLI tools), use SmplClient instead of AsyncSmplClient. The API is identical but without await.
from smplkit import SmplClient, LogLevel
with SmplClient(environment="production", service="my-service") as client:
@client.logging.on_change("sqlalchemy.engine")
def on_sql_change(event):
print(f"SQL level changed to {event.level}")
client.logging.start()Next Steps
- Logging Management — Create and manage loggers programmatically
- Smpl Config — Manage application configuration across environments
- API Reference — Logging — Full REST API documentation

