Skip to content

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.
python
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

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

Key-Scoped Listener

python
@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):

  1. Logger's own environment override
  2. Logger's own base level
  3. Group chain (recursive up the group hierarchy)
  4. Dot-notation ancestry (walk app.paymentsapp)
  5. 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.

python
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=ERRORERROR
  • app.payments → no level, no group → ancestor appERROR
  • sqlalchemy.engine → no level → group databases → group production=WARNWARN

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.

python
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.

python
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.

python
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().

go
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.

python
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