Skip to content

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 (or null to 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 detail
  • DEBUG (1)
  • INFO (2) — default
  • WARN (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:

  1. L.environments[E].levelL's environment-specific override.
  2. L.levelL's base level.
  3. L's group chain — if L.group_id is set, recursively resolve the group's environment override, then base.
  4. L's dot-notation ancestry — walk up acme.payments.stripe_clientacme.paymentsacmeacme (one segment), applying steps 1–3 at each existing ancestor logger.
  5. System fallbackINFO.

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:

  1. Managed loggers themselves.
  2. 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.

  1. Go to Logging in the sidebar.
  2. Find the discovered logger you want to promote.
  3. Click the Default column cell on that row.
  4. 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.

  1. Go to Logging in the sidebar.
  2. Find the discovered logger.
  3. Click an environment column cell on that row.
  4. 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:

bash
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:

bash
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

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

Demote (release)

Setting managed = false releases the logger:

  • level is cleared
  • environments is cleared
  • group_id is cleared
  • The entitlement slot is freed
bash
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
HTTP/1.1 402 Payment Required
json
{
  "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

bash
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 logger
  • GET /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 with managed = 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 by managed, service, last_seen
  • GET /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 set
  • DELETE /api/v1/loggers/{key} — soft-delete
  • POST /api/v1/loggers/bulk — auto-discovery from SDKs (always managed = false)
  • GET /api/v1/loggers/{key}/sources — sources for one logger
  • GET /api/v1/logger_sources — cross-logger source view
  • GET /api/v1/log_groups — list groups
  • POST /api/v1/log_groups — create
  • PUT /api/v1/log_groups/{key} — update
  • DELETE /api/v1/log_groups/{key} — delete (unparents children first)