Skip to content

sp logs ​

Look up the application, Java agent and sp-backend log lines that belong to one request, by its trace_id. Use it after a replay fails to see what happened on both the recording and the replay side.

This needs the unified log pipeline to be enabled on the backend — it is on by default in the single-server install; for Kubernetes see Kubernetes deployment (Helm) — unified log pipeline. When the pipeline is off or unavailable, lookups fail with an error instead of returning an empty result.

Where to get a trace_id: Concepts and IDs — IDs.

Synopsis ​

bash
sp logs --trace-id <id> --since <time> --until <time> [--json]

Flags ​

FlagRequiredDescription
--trace-idYesW3C trace ID of the request
--sinceYesStart of the window, inclusive — ISO-8601 UTC, e.g. 2026-06-27T10:00:00Z
--untilYesEnd of the window, exclusive — ISO-8601 UTC
--jsonNoStandard JSON envelope for scripts and AI agents

All three lookup flags are required. There is no --limit: redirect the output to a file and filter it locally. sp logs has no other filter flags; for filtering by replay run or phase, call the HTTP API below.

Examples ​

bash
sp logs \
  --trace-id 2057ad46a7ce03d3955385f2a4142d29 \
  --since 2026-06-27T10:00:00Z \
  --until 2026-06-27T10:05:00Z \
  --json > /tmp/trace-logs.json

jq '.data.rows | length' /tmp/trace-logs.json
jq -r '.data.rows[] | select(.severity=="ERROR") | "\(.timestamp) \(.source) \(.body)"' /tmp/trace-logs.json | head -20

Without --json, sp logs prints one line per row: timestamp, severity, source, service_name and the message.

HTTP API ​

http
GET /api/recorder/logs?trace_id=<id>[&since=<ts>&until=<ts>][&replay_id=…][&plan_id=…][&plan_item_id=…][&mode=record|replay][&source=…]

The API does more than the command:

  • since / until are optional, as a pair. When you leave both out, the backend works out the windows from the trace itself, each padded by two minutes: one around the recording, and one around the replay given by replay_id — or, without replay_id, around each of the eight most recent replay runs of that trace (older runs are skipped, with a warning). Passing only one of the two is rejected.
  • Very wide windows are replaced. If you pass a window longer than three hours, the backend scans the trace's own windows instead, when it can work them out; if it can't, it scans your window as asked (with a warning), and refuses anything longer than seven days.
  • A long request is scanned at both ends only. If a window worked out from the trace is itself longer than three hours, only 90 minutes at each end are scanned (with a warning).
  • The windows actually scanned are always listed in lookup.windows.
  • Optional filters:
ParameterEffect
replay_idKeep rows from this replay run, plus recording rows (which carry no replay_id). Add mode=replay to drop the recording rows
plan_id, plan_item_idKeep rows from this replay plan or plan item
moderecord or replay
sourceagent, app or backend

Any other parameter is rejected, not ignored.

bash
export SP_API_URL="${SP_API_URL:-http://127.0.0.1:8090}"
TRACE_ID=2057ad46a7ce03d3955385f2a4142d29

# Let the backend pick the windows
curl -s "${SP_API_URL}/api/recorder/logs?trace_id=${TRACE_ID}" \
  -H "Accept: application/json" -o /tmp/trace-logs.json

# Only one replay run
curl -s "${SP_API_URL}/api/recorder/logs?trace_id=${TRACE_ID}&replay_id=<replayId>&mode=replay" \
  -H "Accept: application/json" -o /tmp/trace-logs-replay.json

The API body is at the top level (.rows); sp logs --json wraps the same body in .data (.data.rows).

When the backend can't work out the window ​

The backend can't place a replay window when the replay failed before its first dependency call. With replay_id it says so in warnings; without it there is no warning, so a missing replay window doesn't prove there were no replay logs. A large clock difference between the application and the backend can also put a worked-out window on the wrong minutes. In these cases, query the recording and the replay separately, each with an explicit window:

  1. From sp replay case list --plan <planId> --failed --json, take the case's recordTime (when it was recorded) and requestDateTime (when the replay request was sent; use replayTime if it's empty). Both are epoch milliseconds. If recordTime is empty, use the recording time shown for this trace under Recordings → Rolling recordings.
  2. Query about two minutes either side of each time, with replay_id so other replay runs stay out. If the two windows overlap, query them as one, so no line is fetched twice. Keep the windows and warnings of every response.

Save this as a script (for example case-logs.sh) and run it with bash:

bash
#!/usr/bin/env bash
TRACE_ID=<traceId>; REPLAY_ID=<replayId>
RECORD_MS=<recordTime>; REPLAY_MS=<requestDateTime>
PAD=120000   # two minutes either side
iso() { s=$(( $1 / 1000 )); date -u -d "@$s" +%FT%TZ 2>/dev/null || date -u -r "$s" +%FT%TZ; }
for T in "$RECORD_MS" "$REPLAY_MS"; do
  case "$T" in ''|*[!0-9]*|0) echo "missing timestamp: '$T'" >&2; exit 1;; esac
done
# Two windows, or one if they overlap, so no log line is fetched twice
A=$(( RECORD_MS < REPLAY_MS ? RECORD_MS : REPLAY_MS )); B=$(( RECORD_MS < REPLAY_MS ? REPLAY_MS : RECORD_MS ))
if [ $(( B - PAD )) -le $(( A + PAD )) ]; then WINDOWS=("$((A - PAD)):$((B + PAD))")
else WINDOWS=("$((A - PAD)):$((A + PAD))" "$((B - PAD)):$((B + PAD))"); fi
i=0; FILES=()
for W in "${WINDOWS[@]}"; do
  i=$((i+1)); out="/tmp/logs-${TRACE_ID}-${i}.json"; FILES+=("$out")
  curl -sf "${SP_API_URL}/api/recorder/logs?trace_id=${TRACE_ID}&replay_id=${REPLAY_ID}&since=$(iso "${W%%:*}")&until=$(iso "${W##*:}")" \
    -H "Accept: application/json" -o "$out" || { echo "request $i failed" >&2; exit 1; }
  jq -e 'has("rows")' "$out" >/dev/null || { echo "request $i: $(jq -c . "$out")" >&2; exit 1; }
done
# Keep every window, warning and row
jq -s '{windows: [.[].lookup.windows[]?], warnings: [.[].warnings[]?], rows: ([.[].rows[]] | sort_by(.timestamp))}' "${FILES[@]}"

If the application's clock differs from the backend's by more than a couple of minutes, shift the windows by the difference or widen them. For a request that itself ran for hours, query consecutive windows of at most three hours each rather than just its start and end.

Don't pass one window that stretches from the recording time to the replay time: it spans every minute in between and is slow, or is refused.

Output ​

FieldMeaning
lookuptype (trace), value (the trace ID) and windows — the time windows actually scanned
rowsLog lines in time order — see Log query fields
warningsNotices that didn't stop the query but may mean the result is incomplete: a window that couldn't be worked out, replay runs that were skipped, a window that was replaced or cut short. Read them before concluding that logs are missing
json
{
  "ok": true,
  "command": "logs",
  "data": {
    "lookup": {
      "type": "trace",
      "value": "2057ad46a7ce03d3955385f2a4142d29",
      "windows": [
        { "since": "2026-06-27T10:00:00Z", "until": "2026-06-27T10:05:00Z" }
      ]
    },
    "rows": [
      {
        "timestamp": "2026-06-27T10:00:10.123Z",
        "severity": "WARN",
        "body": "Replay comparison mismatch",
        "service_name": "sp-backend",
        "source": "backend",
        "trace_id": "2057ad46a7ce03d3955385f2a4142d29",
        "span_id": "8d10c94a2a6f4e11",
        "replay_id": "6891fd300c676b31",
        "effective_mode": "replay"
      }
    ],
    "warnings": []
  }
}

Reading the result ​

bash
# Rows per source
jq '[.data.rows[].source] | group_by(.) | map({source: .[0], n: length})' /tmp/trace-logs.json
# Notices
jq '.data.warnings' /tmp/trace-logs.json
# Backend errors first, then agent, then app
jq -r '.data.rows[] | select(.source=="backend" and .severity=="ERROR") | "\(.timestamp) \(.body)"' /tmp/trace-logs.json | head -20
What you seeLikely causeNext step
No rows, warnings not emptyThe window could not be derived, or the backend's log reader doesn't match the stored schemaRead the warning; pass since/until explicitly, or upgrade sp-backend
No rows, no warningsWrong trace_id, wrong window, or the logs haven't been written yetTake the trace_id from the failed replay case; widen the window; try again a few minutes later
Only backend rowsThe agent isn't exporting logs, or the app logs nothing for this requestCheck that the agent is attached and can reach the backend; check the app's log levels
Rows from agent, app and backendThe pipeline is fineRead the ERROR/WARN lines, then look at the diff with sp diagnose

To see whether the backend actually sent the replayed request to your service, filter backend rows for Replay send start / done / failed — see Replay send log markers.

Errors ​

Invalid input and backend errors use the standard stderr envelope (see Output contract):

json
{
  "ok": false,
  "command": "logs",
  "error": {
    "code": "API_ERROR",
    "message": "API error 1: log pipeline is disabled",
    "httpStatus": 200,
    "backend": { "responseCode": 1, "responseDesc": "log pipeline is disabled" }
  }
}

Messages you may see: trace_id is required, since is required, until is required, since must be before until, since and until must be ISO-8601 UTC timestamps, unsupported logs query parameter: <name>, log pipeline is disabled, log pipeline is unavailable.

Removed commands ​

These older log commands and endpoints no longer exist:

RemovedUse instead
sp recorder logs, sp recorder query, sp recorder info, sp querysp logs, or the HTTP API above
sp record logs overview, sp record logs downloadsp logs … > file
sp replay logs (including --overview)sp logs, or the API with replay_id
--include-recording-logNot needed: recording and replay share one trace_id
source_summary in the responsejq grouping by source (see above)
GET /api/record-logs/*, GET /api/replay-logs/*GET /api/recorder/logs?trace_id=…

Capture Sessions · Review Steps · Improve what matters