Smpl Logging overview
Smpl Logging gives you runtime control over your application's log levels — dial acme.payments.stripe_client from INFO to DEBUG in production without redeploying. The platform discovers every logger your code knows about, lets you configure the ones you care about, and pushes level changes to running services over WebSocket.
A concrete example
Your payments service uses Python's logging module. On the first run, the smplkit logging adapter monkey-patches Manager.getLogger so every category that gets touched — acme.payments, sqlalchemy.engine, urllib3.connectionpool, boto3 — is reported to the platform. They all show up in the console as discovered, with their currently-resolved level shown in dim text per environment.
A bug needs investigation. You go to the console, click acme.payments.stripe_client in the staging column, and pick DEBUG. The platform pushes a level-change event over WebSocket; the running service applies it within milliseconds. No redeploy.
Loggers
A logger is one named category in your application's logging framework — com.acme.payments, sqlalchemy.engine, MyApp.Db. The platform stores loggers in a single table with a discriminator column distinguishing loggers from groups.
Loggers carry:
level— the base log level when set (ornullto inherit).environments[env].level— per-environment override.group_id— optional reference to a log group.managed— whether the platform actively controls this logger (true) or only observes it (false, the default for auto-discovered loggers).
Logger names are normalized: / and : are replaced with ., and the whole name is lowercased. So Acme/Payments, acme:payments, ACME.PAYMENTS, and acme.payments all refer to the same logger.
Log levels
Seven levels in increasing severity:
TRACE(0) — finest detailDEBUG(1)INFO(2) — defaultWARN(3)ERROR(4)FATAL(5)SILENT(6) — fully muted
SILENT is the equivalent of disabling a logger.
Log groups
A log group is a named bundle of loggers that share a level. Useful when you have a cross-service concern — "all database loggers", "all integration test loggers" — that doesn't follow your code's dot-notation hierarchy.
Groups have their own levels and per-environment overrides; loggers assigned to a group inherit from the group when their own level is null. A logger belongs to at most one group.
Groups are flat in v1 — a group cannot contain another group. The API rejects nested groups.
Groups are always managed (there's no "discovered group" concept — you create groups explicitly). They count toward the logging.groups entitlement.
Level resolution algorithm
For a logger L evaluated in environment E, the SDK walks the resolution chain and returns the first non-null level it finds:
L.environments[E].level—L's environment-specific override.L.level—L's base level.L's group chain — ifL.group_idis set, recursively resolve the group's environment override, then base.L's dot-notation ancestry — walk upacme.payments.stripe_client→acme.payments→acme→acme(one segment), applying steps 1–3 at each existing ancestor logger.- System fallback —
INFO.
Group precedence over dot-notation ancestry is intentional: group assignment is an explicit customer action, so it should override the implicit dot-notation hierarchy.
The same algorithm runs in the SDK and in the console — both resolve sparse data the same way.
What the SDK applies vs leaves alone
The SDK is conservative about modifying your runtime. It explicitly applies levels to:
- Managed loggers themselves.
- Descendants of managed loggers (server-known or discovered at runtime).
Unmanaged loggers with no managed ancestor are not touched at runtime. The framework's own level configuration wins. This avoids surprising behavior when a discovered logger doesn't yet have any platform configuration — you keep getting whatever the framework was producing.
Discovered vs managed
Auto-discovered loggers start with managed = false. They're visible in the console but read-only; the dim text shows resolved_level reported from running services. They don't count toward your logging.managed_loggers quota.
A managed logger (managed = true) is platform-controlled — the SDK actively applies platform-resolved levels back to your framework's logger. It counts against quota.
See Discovered vs managed for the cross-product mental model.
Promote to managed
Promote an auto-discovered logger to managed so the platform actively controls its level. Prerequisites: at least MEMBER role; your logging.managed_loggers quota must have a slot free.
The first promotion of a logger runs a one-time descendant cascade: every dot-notation descendant that's still unmanaged gets level = NULL, so the resolution chain inherits from the newly-managed ancestor.
In the console
Two click paths in the logger grid:
Promote with one universal level.
- Go to Logging in the sidebar.
- Find the discovered logger you want to promote.
- Click the Default column cell on that row.
- Pick a level (
TRACE,DEBUG,INFO,WARN,ERROR,FATAL,SILENT).
Result: managed = true, level = <chosen>, environments = {} — apply this level everywhere.
Promote with an environment-specific level.
- Go to Logging in the sidebar.
- Find the discovered logger.
- Click an environment column cell on that row.
- Pick a level.
Result: managed = true, level = "DEBUG" as the base, environments[<env>].level = <chosen> — apply only to that environment, leave other environments at DEBUG.
Via the API
The same PUT /api/v1/loggers/{id} endpoint handles both promotion paths and ordinary level changes. The promotion is implicit — when the body sets level, group, or non-empty environments on a currently-unmanaged logger and doesn't explicitly set managed: false, the platform auto-promotes (sets managed = true).
Universal-level promote:
curl -X PUT https://app.smplkit.com/api/v1/loggers/acme.payments \
-H "Authorization: Bearer $SMPLKIT_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "logger",
"id": "acme.payments",
"attributes": {
"level": "WARN",
"environments": {}
}
}
}'Environment-specific promote:
curl -X PUT https://app.smplkit.com/api/v1/loggers/acme.payments \
-H "Authorization: Bearer $SMPLKIT_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "logger",
"id": "acme.payments",
"attributes": {
"level": "DEBUG",
"environments": {
"production": {"level": "WARN"}
}
}
}
}'Via the SDK
import asyncio
from smplkit import AsyncSmplManagementClient, LogLevel
async with AsyncSmplManagementClient() as manage:
payments = manage.loggers.new("acme.payments") # creates if missing
payments.set_level(LogLevel.DEBUG) # base
payments.set_level(LogLevel.WARN, environment="production")
await payments.save() # promotes implicitlyDemote (release)
Setting managed = false releases the logger:
levelis clearedenvironmentsis clearedgroup_idis cleared- The entitlement slot is freed
curl -X PUT https://app.smplkit.com/api/v1/loggers/acme.payments \
-H "Authorization: Bearer $SMPLKIT_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-d '{
"data": {
"type": "logger",
"id": "acme.payments",
"attributes": {"managed": false}
}
}'After release, the framework's own level wins again. The SDK stops applying platform-resolved levels to that logger.
The platform also auto-demotes silently: if a managed logger ends up with no level, no environments, and no group_id after a save, it's flipped back to managed = false to free the slot. (Auto-demote does not trigger the descendant cascade.)
Quota and 402
If your account is at the logging.managed_loggers cap when you promote:
HTTP/1.1 402 Payment Required{
"errors": [{
"status": "402",
"code": "entitlement_limit_reached",
"title": "Subscription limit reached",
"detail": "Your free plan allows a maximum of 15 managed loggers. Upgrade your subscription to increase this limit.",
"meta": {
"limit_key": "logging.managed_loggers",
"current": 15,
"maximum": 15,
"plan": "free"
}
}]
}Either upgrade your Logging subscription, or release a different managed logger first. See Subscriptions and entitlements.
Verify
curl https://app.smplkit.com/api/v1/loggers/acme.payments \
-H "Authorization: Bearer $SMPLKIT_API_KEY"After promotion: data.attributes.managed is true, level and/or environments reflect what you set.
After release: managed is false; level, environments, group_id are cleared.
In a running service, the framework's logger reflects the platform-resolved level within a few seconds via WebSocket push.
Logger sources
logger_source rows track per-(logger, service, environment) observation: when the SDK first reported the logger, when it was last seen, the level the framework reported, and the resolved level. Two endpoints:
GET /api/v1/loggers/{id}/sources— sources for one loggerGET /api/v1/logger_sources?service=...&environment=...— cross-logger view
This is what powers the "where is this logger running?" question — a managed logger you stop using somewhere will still resolve to its configured level on services that boot it, and the source's last_seen tells you which.
Limits
Plan-driven (entitlement keys):
logging.managed_loggers— loggers withmanaged = true. Returns 402 on the promoting PUT.logging.groups— log groups. Returns 402 on group creation.
Hard caps (platform-wide):
- 10,000 logger rows per account.
- 100,000 logger source rows per account.
API surface
GET /api/v1/loggers— list, filter bymanaged,service,last_seenGET /api/v1/loggers/{key}— fetch one (key is the normalized logger name)PUT /api/v1/loggers/{key}— upsert (creates if missing); promotes if discovery fields are setDELETE /api/v1/loggers/{key}— soft-deletePOST /api/v1/loggers/bulk— auto-discovery from SDKs (alwaysmanaged = false)GET /api/v1/loggers/{key}/sources— sources for one loggerGET /api/v1/logger_sources— cross-logger source viewGET /api/v1/log_groups— list groupsPOST /api/v1/log_groups— createPUT /api/v1/log_groups/{key}— updateDELETE /api/v1/log_groups/{key}— delete (unparents children first)
Related
- Smpl Logging Management — SDK and API patterns
- Smpl Logging Runtime — installing adapters and applying levels
- Discovered vs managed
- API Reference — Logging

