Skip to content

Logs

Included free on every install, including retention policies and volume alerts by email. Slack, Discord, PagerDuty and webhook channels on a volume alert need a license with the alerts-pro feature. See pricing.

Every line the instance logs is stored, so you can search it later, watch it live and keep it for as long as you choose. You can turn on debug output for one tenant without a restart, and send every record to Loki or Elasticsearch as well.

ViewWhat it readsUse it to
SearchEvery stored record, kept by the retention policy.Find lines from yesterday or last month by text, level, request or trace.
StreamThe most recent records the instance holds in memory.Follow the live log, optionally starting with what is already buffered.
TailRecords as they are written, starting with the last 100.Follow the live log with pause and resume.

Records are written to the database within about a second. An admin reads only their own tenant's records in every view. A super admin reads one tenant in search and tail (the default tenant, or the one named in tenant_id), and every record in the stream, including the instance's own lines that belong to no tenant.

You need an admin token in TOKEN. The quickstart shows how to get one.

  1. Make a request and give it an id you choose. The instance accepts your X-Request-ID and logs it with the request:

    Terminal window
    curl -s http://localhost:3001/api/admin/logging/levels \
    -H "Authorization: Bearer $TOKEN" \
    -H "X-Request-ID: try-logs-1"

    The answer is the current levels: {"default_level": "INFO", "tenants": {}, "plugins": {}}.

  2. Search for that id:

    Terminal window
    curl "http://localhost:3001/api/admin/logs/search?request_id=try-logs-1" \
    -H "Authorization: Bearer $TOKEN"
    {
    "results": [
    {
    "timestamp": "2026-10-01T09:12:44.067704Z",
    "level": "INFO",
    "message": "request",
    "tenant_id": "default",
    "trace_id": "00000000000000000000000000000000",
    "span_id": "0000000000000000",
    "request_id": "try-logs-1",
    "fields": { "bytes": 51, "duration_ms": 1, "method": "GET", "path": "/api/admin/logging/levels", "status": 200 }
    }
    ],
    "total": 1,
    "limit": 100,
    "offset": 0
    }
  3. Turn on debug output for your tenant:

    Terminal window
    curl -X PUT http://localhost:3001/api/admin/logging/levels/tenant/default \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"level": "DEBUG"}'
    { "tenant": "default", "level": "DEBUG" }
  4. Follow the live log, starting with what is buffered. Press Ctrl+C to stop:

    Terminal window
    curl -N "http://localhost:3001/api/admin/logs/stream?min_level=DEBUG&from_head=true" \
    -H "Authorization: Bearer $TOKEN"

    You see log events, then replay_complete and connected, then new lines as they happen. A stream event gives level as a number: -4 is DEBUG, 0 is INFO, 4 is WARN and 8 is ERROR.

  5. Put the level back with the same request as step 3 and {"level": "INFO"}. In the admin console, Insight > Logs searches the same records, and Insight > Observability shows the live stream under Live log tail.

GET /api/admin/logs/search takes these filters, all optional:

FilterMatches
queryWords in the message.
levelExactly one level: DEBUG, INFO, WARN or ERROR.
tenant_idOne tenant. Only a super admin can name a tenant other than their own.
pluginThe feature that wrote the line, by its name in LYEVE_PLUGINS. Today this finds almost nothing, see the caution below.
request_id, trace_idOne request, or one distributed trace.
from, toA time range in RFC 3339, such as 2026-10-01T00:00:00Z.
limit, offsetPaging. limit defaults to 100, at most 1000. Newest first.

Both live views are Server-Sent Events, so curl -N or a browser EventSource can read them.

  • GET /api/admin/logs/stream takes min_level, tenant_id, plugin, keyword, since (a sequence number, to resume after a disconnect) and from_head=true to send the buffered records first. Its events are log, replay_complete after a replay, and connected. A heartbeat comment every 30 seconds keeps proxies from closing it.
  • GET /api/admin/logs/tail takes level (one level, or several separated by commas), plugin and query. Its first event, stream-id, carries an id. It then sends the last 100 matching records as replay events, and new ones as log. Pause and resume it with POST /api/admin/logs/tail/pause and /resume and a body of {"stream_id": "<id>"}.

Levels are DEBUG, INFO, WARN and ERROR, and the default is INFO. A level change applies at once and is saved, so it survives a restart.

RequestChangesWho
PUT /api/admin/logging/levels/tenant/{tenant}One tenant's level.An admin for their own tenant, a super admin for any.
PUT /api/admin/logging/levels/plugin/{name}?tenant={tenant}One feature's level in one tenant. {name} is its name in LYEVE_PLUGINS.As above.
PUT /api/admin/logging/levels/plugin/{name}One feature's level in every tenant.Super admin.
PUT /api/admin/logging/levelsAll levels at once: default_level, tenants, plugins.Super admin.

Each body is {"level": "DEBUG"}, except the last, which takes the same shape GET /api/admin/logging/levels returns.

Every record is stored in the database. A super admin can add sinks that send each record somewhere else as well:

Terminal window
curl -X PUT http://localhost:3001/api/admin/logging/sinks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[{"driver": "loki", "endpoint": "https://loki.example.com", "labels": {"env": "prod"}}]'

The body replaces the whole list. An endpoint on a private, loopback or link-local address, localhost, or a name ending in .local or .internal is refused with 400. Reading the sinks back shows credentials as [REDACTED].

Sink settings
KeyMeaning
driverstdout, loki or elasticsearch.
endpointThe Loki or Elasticsearch address, http or https.
labelsStatic labels added to every record.
api_key, username, passwordCredentials for Elasticsearch. A Loki credential goes in the URL.
index_prefixElasticsearch index prefix.
batch_size, max_bufferHow many records are sent at once, and how many wait at most.
http_timeout_secsDefaults to 10 for Loki and 30 for Elasticsearch.
tls_insecureSkips certificate checks. Use it only for testing.

GET and PUT /api/admin/logging/config read and replace the configuration in one document: sinks, levels, and redacted_fields, a list of field names left out of search, tail and stream output. A super admin's read also carries volume_alerts, and a write keeps the stored volume rules whatever it sends. Writing it needs a super admin. GET /api/admin/logging/dashboard/queries returns ready-made LogQL queries for Grafana panels.

The default policy keeps ERROR and WARN for 90 days, DEBUG for 7, and everything else for 30. Expired records are removed every hour, and saving a policy removes what it has expired at once. The answer reports how many records went in purged.

Terminal window
curl -X PUT http://localhost:3001/api/admin/logging/retention \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"policies": {"default_days": 30, "per_level": {"ERROR": 180, "DEBUG": 3}}, "per_tenant": {"acme": {"default_days": 60}}}'

A level with no entry in per_level uses default_days. The answer also carries schedule, a cron expression that changes nothing: the purge runs every hour whatever it says. A super admin's save replaces the global policy and every tenant's entry. An admin's save sets only their own tenant's entry, and their GET shows the global policy and that entry alone.

GET /api/admin/logging/volume?window=1h counts records by level, tenant and feature over the window, such as 15m, 24h or 7d. A value it cannot read counts the last hour.

A super admin sets volume rules with PUT /api/admin/logging/alerts:

{
"thresholds": [
{ "id": "errors", "level": "ERROR", "max_count": 500, "window": "5m", "enabled": true,
"channels": { "email": ["ops@example.com"] } }
],
"cooldown": "10m"
}
FieldMeaning
idNames the rule. A rule keeps it across writes, and one sent without it gets one.
levelThe level counted. Empty counts every level.
tenant_id, pluginCount only that tenant's or that feature's entries. A rule with no tenant counts every tenant.
max_countHow many entries within window fire the rule.
windowHow far back the rule counts, such as 5m or 1h.
enabledWhether the rule is checked.
channelsWhere the alert goes: email on every install, and Slack, Discord, PagerDuty or a signed webhook with alerts-pro. See Alerts.
cooldownThe shortest time between two alerts from one rule.

The rules are checked every minute in the background, on one replica at a time. A rule that fires sends its alert to the live tail as an ALERT record, to the log as a warning, and to its channels. Reading the volume lists in its alerts the rules your volume breaks right now, and sends nothing.

A super admin reads every rule. An admin reads only the rules scoped to their own tenant, with each email address shortened, and GET on the configuration leaves the rules out for anyone but a super admin. PUT on the configuration keeps the stored rules, so change them only through the alerts routes.

Each rule can also be handled on its own. POST /api/admin/logging/alerts adds one rule and gives it an id. GET, PUT and DELETE /api/admin/logging/alerts/{id} read, replace and remove one, and answer the rule with licensed beside its fields. They follow the same roles, masking and license check as the list, and a rule a tenant admin may not read answers 404, as a missing one does. Writes to the rules from several replicas at once never lose one.

A refusal on these routes carries a code beside its message, so a client can branch on it:

StatusCodes
400logging.invalid_body, logging.empty_window, logging.invalid_max_count, logging.invalid_window, logging.duplicate_rule_id
404logging.rule_not_found
422logging.masked_channel, logging.invalid_channel, logging.channel_target_refused
503logging.channels_unavailable, logging.secret_unavailable, logging.alert_store_failed

Every route needs an admin or super admin. Writing the global levels, the sinks, the whole configuration and the volume thresholds needs a super admin. An admin sees only their own tenant's records. An admin token with the logs:read grant can call search, logs/export and logs/stats.

This feature has no environment variables. Everything above is set over the API or in the console and saved in the database. Logs run on every install. If you set LYEVE_PLUGINS to choose which features start, include logging in it, or the instance only writes to stdout and keeps its recent records in memory. See licensing and tiers.

The ones you are most likely to meet:

StatusWhen
400invalid level: <level>, or a sink endpoint was refused.
403A tenant_id outside your tenant, or a super admin route.
503log tailer not available or log ring not available.
Every error message
StatusMessage
400invalid request body
400query contains invalid characters
400invalid level: <level>
400missing tenant path parameter
400per_tenant keys must not be empty
400missing or invalid stream_id
400sink[<n>] endpoint rejected: <reason>
400threshold for "<level>" has empty window, ... has non-positive max_count
400two thresholds share one id
402payment_required, naming feature:alerts-pro, on a paid alert channel
422An alert channel that breaks a rule on Alerts
403tenant_id must match the authenticated tenant
403access denied: cannot modify another tenant's log level
403instance-wide logging configuration requires super_admin
404stream not found
503log tailer not available, log ring not available
503alert channels other than email cannot be stored right now
Search, stream and tail
MethodPathPurpose
GET/api/admin/logs/searchSearch stored records.
GET/api/admin/logs/streamLive stream of recent records.
GET/api/admin/logs/exportRecent in-memory records as JSON: entries, total, next_seq, truncated. limit defaults to 1000, at most 10000. Takes the stream filters.
GET/api/admin/logs/statsThe in-memory buffer: entries, capacity, sequence, subscribers.
GET/api/admin/logs/tailLive tail.
POST/api/admin/logs/tail/pausePause a tail: {"stream_id": "<id>"}.
POST/api/admin/logs/tail/resumeResume it.
GET/api/admin/logs/tail/exportRecent tail records as JSON. Takes the tail filters and limit (default 500, at most 10000).
GET/api/admin/logs/tail/statusavailable and subscriber_count.
Levels, sinks, retention and alerts
MethodPathPurposeWho
GET/api/admin/logging/levelsdefault_level, tenants, plugins.Admin
PUT/api/admin/logging/levelsReplace all levels.Super admin
PUT/api/admin/logging/levels/tenant/{tenant}One tenant's level.Admin
PUT/api/admin/logging/levels/plugin/{name}One feature's level.Admin with ?tenant=, else super admin
GET, PUT/api/admin/logging/sinksRead or replace the sinks.Admin reads, super admin writes
GET, PUT/api/admin/logging/configRead or replace the whole configuration.Admin reads, super admin writes
GET, PUT/api/admin/logging/retentionRead or set retention.Admin
GET/api/admin/logging/dashboard/queriesLogQL queries for Grafana.Admin
GET/api/admin/logging/volumeRecord counts over window.Admin
GET, PUT/api/admin/logging/alertsRead or replace the volume rules and their channels. The read carries licensed.Admin reads their tenant's rules, super admin reads all and writes
POST/api/admin/logging/alertsAdd one volume rule.Super admin
GET/api/admin/logging/alerts/{id}One volume rule, with licensed.Admin, for a rule they may read
PUT, DELETE/api/admin/logging/alerts/{id}Replace or remove one volume rule.Super admin