JSON Output

May 3, 2026 · View on GitHub

The --json flag switches Ember to a streaming JSONL (JSON Lines) mode. One JSON object is printed to stdout per polling interval, making it easy to pipe into other tools.

ember --json

Errors are printed to stderr, so stdout always contains valid JSONL.

Output Schema

Each line is a JSON object with the following fields:

{
  "threads": {
    "threadDebugStates": [...],
    "reservedThreadCount": 0
  },
  "metrics": {
    "totalThreads": 0,
    "busyThreads": 0,
    "queueDepth": 0,
    "workers": {},
    "httpRequestsTotal": 0,
    "httpRequestDurationSum": 0,
    "httpRequestDurationCount": 0,
    "httpRequestsInFlight": 0,
    "hasHttpMetrics": true,
    "hosts": {}
  },
  "process": {
    "pid": 12345,
    "cpuPercent": 2.5,
    "rss": 52428800,
    "createTime": 1710000000000,
    "uptime": 3600000000000
  },
  "fetchedAt": "2026-03-16T10:00:00Z",
  "errors": [],
  "derived": {
    "rps": 150.5,
    "avgTime": 12.3,
    "p50": 8.0,
    "p95": 25.0,
    "p99": 80.0
  },
  "hosts": [
    {
      "host": "example.com",
      "rps": 100.2,
      "avgTime": 10.5,
      "inFlight": 3,
      "p50": 7.0,
      "p90": 15.0,
      "p95": 22.0,
      "p99": 75.0,
      "statusCodes": { "200": 95.0, "404": 3.0, "500": 2.0 },
      "methodRates": { "GET": 80.0, "POST": 20.0 }
    }
  ]
}

Field Reference

FieldDescription
threadsRaw FrankenPHP thread debug states (empty if Caddy-only)
metricsRaw Caddy and FrankenPHP metrics from the admin API
processMonitored process info: PID, CPU %, RSS (bytes), uptime
fetchedAtTimestamp of this poll (RFC 3339)
errorsArray of error strings from this poll (omitted if empty)
derivedComputed metrics: RPS, average response time, error rate, percentiles (omitted on first poll)
derived.errorRateMiddleware errors per second (omitted when 0)
derived.p50/p95/p99Request duration percentiles in ms (omitted when unavailable)
hostsPer-host breakdown (omitted when no host-level data)
hosts[].errorRateMiddleware errors per second for this host (omitted when 0)
hosts[].ttfbP50/P90/P95/P99Time-to-First-Byte percentiles in ms (omitted when unavailable)
hosts[].statusCodesStatus code → rate (req/s)
hosts[].methodRatesHTTP method → rate (req/s)
hosts[].avgRequestSizeAverage request body size in bytes (omitted when 0)
upstreamsReverse proxy upstream health (omitted when no reverse_proxy is configured)
upstreams[].addressUpstream address (host:port)
upstreams[].handlerReverse proxy handler name (omitted when Caddy doesn't expose the label)
upstreams[].healthyWhether the upstream is healthy
upstreams[].healthChangedWhether health status changed since last poll (omitted when false)

Single Snapshot

The --once flag outputs a single JSON object and exits, without needing to kill the process:

ember --json --once

This is useful for scripting, CI pipelines, or feeding data into other tools:

# Get current state as JSON
ember --json --once | jq '.process.cpuPercent'

# Check if any host has 5xx errors
ember --json --once | jq -e '.hosts[] | select(.statusCodes["500"] > 0)'

Note: Derived metrics (RPS, average latency, percentiles) require two data points and will be zero on a single snapshot since there is no previous poll to compute a delta.

Scripting Examples

Watch RPS in real time

ember --json | jq -r '.derived.rps'

Extract per-host 5xx rates

ember --json | jq -r '.hosts[] | "\(.host): \(.statusCodes["500"] // 0)/s"'

Log to file with timestamps

ember --json --interval 5s >> ember.log

Tip: Combine --json with --interval to control the output rate. For example, ember --json --interval 5s outputs one line every 5 seconds.

Multi-instance output

When --addr is repeated, Ember polls every instance per tick and emits one JSONL line per instance, prefixed with an instance field:

ember --json \
  --addr web1=https://web1.fr \
  --addr web2=https://web2.fr
{"instance":"web1","threads":{...},"metrics":{...},"hosts":[...],"fetchedAt":"..."}
{"instance":"web2","threads":{...},"metrics":{...},"hosts":[...],"fetchedAt":"..."}
{"instance":"web1","threads":{...},"metrics":{...},"hosts":[...],"fetchedAt":"..."}

The first emission is sorted alphabetically by name. Subsequent lines interleave per instance as each one polls on its own ticker (per-instance ,interval= suffix when set, otherwise the global --interval), so downstream consumers should expect lines in arrival order rather than grouped per tick. With --once, exactly one line per instance is produced in alphabetical order before exit.

When only a single --addr is provided, the instance field is omitted: the output is byte-for-byte identical to the pre-multi-instance format.

Multi-instance JSONL snapshots can be diffed with ember diff: lines are grouped by instance, the last entry per instance wins, and one diff block is emitted per alias.

See Also