Skip to content
Coder Docs

Send Agent Relay logs to a SIEM

Ingest Agent Relay logs into a SIEM, with the record shape, fields, parsing rules, and log-based alerts.

On this page

Note

Agent Relay is in beta. Log messages and fields can change between beta releases.

This page is for teams that ingest Agent Relay logs into a security information and event management (SIEM) system or another log platform that reads JSON lines. Agent Relay doesn't emit a separate event ID, so build searches and alerts on msg, level, logger_names, and values inside fields.

Set logFormat: json and ship the container's stderr to your SIEM with your usual log collector, as you would for Coder service logs.

Log levels

Each logLevel setting includes every more severe level:

logLevelRecords written
debugDEBUG, INFO, WARN, ERROR
infoINFO, WARN, ERROR
warnWARN, ERROR
errorERROR

Use info for SIEM ingestion. debug adds routine retries, poll backpressure, and license checks without adding alerting value. For Claude Code, debug also writes work order payload records that contain the decoded work order, including emails and repository URLs.

Record shape

Each JSON record has these top-level keys:

KeyMeaning
tsEvent time in UTC, as RFC 3339 with nanosecond precision
levelDEBUG, INFO, WARN, or ERROR
msgThe event message
callerSource file and line of the log statement. Changes between builds.
funcGo function that wrote the record. Changes between builds.
logger_namesThe Agent Relay service that wrote the record, such as claude-code-runners, cursor-workers, reaper, or coderd for startup
fieldsEvent data, including the correlation fields and the fields below

The reaper deletes finished and failed workspaces for both providers. Its records carry workspace_id and workspace_name, but no provider, pool, or session ID.

Provider-specific fields

Both providers log the correlation fields. Each provider also logs identity fields and aliases that use its own API's names. An alias always carries the same value as the field it mirrors.

ProviderFieldMeaning
Claude Codesession_uuidAlias of session_id. Nack records written after a runner failure carry only session_uuid.
Claude CodejtiAlias of delivery_id, the work order's JWT ID
Claude CodeattemptAnthropic's delivery counter for the session, as a string such as "0". Increases on each redelivery.
Claude Codespawn_reasonsession_created for a new session, or runner_lost when the runner for an existing session went away
Claude Codeaccount_idAnthropic account ID (user_...) of the user who started the session. Empty for Claude Tag channel sessions.
Claude Codeaccount_emailAnthropic-attested email used to find the Coder user. Empty for Claude Tag channel sessions.
Claude Codeagent_idClaude Tag agent ID (cagt_...) that started the session. Empty for most user sessions.
Claude Codeorg_service_nameAnthropic service that started an agent session, such as anthropic:slackbot. Empty for user sessions.
Claude CodeusernameAlias of owner
Claude Codeuser_idAlias of owner_id, the Coder user UUID
Cursorrequest_idAlias of session_id, Cursor's pending request ID (bc-...)
Cursorworker_idAlias of delivery_id, the cw_... worker ID that Agent Relay sends to Cursor with the claim
Cursorprovider_user_idCursor's numeric user ID, logged before Agent Relay finds the Coder user
Cursoruser_idAlias of provider_user_id. Unlike Claude Code's user_id, this is a Cursor ID, not a Coder user UUID.
Cursoruser_emailThe requester's email, on records for requests that Agent Relay refuses

Because user_id means different things for each provider, use owner_id for the Coder user in reports that cover both.

Startup records also echo pool configuration, such as organization, organization_id, and template. Claude Code adds claude_tag_channel_user, and Cursor adds cursor_pool, relist_interval, and relist_jitter.

Failure and rebuild fields

FieldMeaning
errorError detail on most WARN and ERROR records, such as a failed claim, identity lookup, build, or delete
retryabletrue when the provider retries or the user can try again. false when an administrator must fix configuration or identity.
messageCursor: the exact text shown to the user when Agent Relay fails the claim
detailCursor: the local reason for a failed claim. It's never sent to Cursor and is truncated at 512 characters.
stageCursor: the step where the claim failed, such as owner or building
reasonReaper: why it deleted the workspace, such as the runner exited with status 1. Cursor refusals: no_email, no_match, inactive, or dormant.
job_statusThe final provisioner job status of a failed build, such as failed or canceled
job_errorCoder's error summary for a failed build
build_statusThe latest build's status when Agent Relay rebuilds a workspace or the reaper evaluates it
deadlineClaude Code: when Anthropic stops waiting for a runner. Logged when the deadline passes before dispatch.
statusReaper: the runner status the workspace reported, such as working, done 1, failed <reason>, or orphaned
ageReaper: time elapsed after the workspace's latest build was created
failed_deletesReaper: how many delete builds have failed in a row for the workspace

Claude Code nack records don't repeat the failure reason. Find it in the error or reason field of the WARN record written just before the nack.

Example records

A Claude Code workspace build starting:

{
  "ts": "2026-10-14T16:02:11.912540118Z",
  "level": "INFO",
  "msg": "workspace build created",
  "caller": "/src/internal/workspacedispatcher/dispatcher.go:344",
  "func": "github.com/coder/agent-relay/internal/workspacedispatcher.(*Dispatcher).createWorkspace",
  "logger_names": ["claude-code-runners"],
  "fields": {
    "provider": "claude_code",
    "pool": "prod",
    "session_id": "00000000-1111-4222-8333-444444444444",
    "session_uuid": "00000000-1111-4222-8333-444444444444",
    "delivery_id": "77777777-6666-4555-8444-333333333333",
    "jti": "77777777-6666-4555-8444-333333333333",
    "attempt": "0",
    "client_platform": "web_claude_ai",
    "spawn_reason": "session_created",
    "account_id": "user_01AAAAAAAAAAAAAAAAAAAAAAAA",
    "account_email": "[email protected]",
    "agent_id": "",
    "org_service_name": "",
    "owner": "jdoe",
    "owner_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
    "username": "jdoe",
    "user_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
    "workspace_id": "99999999-8888-4777-8666-555555555555",
    "workspace_name": "cc-00000000-1111-4222-8333-44444"
  }
}

A Cursor claim that failed after a build error. The user sees message in Cursor:

{
  "ts": "2026-10-14T16:04:37.391402118Z",
  "level": "WARN",
  "msg": "failed the claim upstream, the user sees the message",
  "caller": "/src/internal/services/servicecursorworkers/fail.go:68",
  "func": "github.com/coder/agent-relay/internal/services/servicecursorworkers.(*Service).failClaim",
  "logger_names": ["cursor-workers"],
  "fields": {
    "provider": "cursor",
    "pool": "prod",
    "session_id": "bc-00000000-1111-4222-8333-444444444444",
    "request_id": "bc-00000000-1111-4222-8333-444444444444",
    "provider_user_id": 4242,
    "user_id": 4242,
    "delivery_id": "cw_77777777-6666-4555-8444-333333333333",
    "worker_id": "cw_77777777-6666-4555-8444-333333333333",
    "owner": "jdoe",
    "owner_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
    "stage": "building",
    "retryable": true,
    "message": "agent-relay: workspace build failed. Try again in a few minutes.",
    "detail": "workspace cur-bc-00000000-1111-4222-8333-4 job failed: terraform apply: exit status 1"
  }
}

ID prefixes show where a value comes from:

PrefixMeaning
cc-Claude Code workspace name: cc- and the session UUID, truncated to 32 characters
cur-Cursor workspace name: cur- and the lowercased Cursor request ID, truncated to 32 characters
bc-Cursor request ID. Share it with Cursor support.
cw_Cursor worker ID that Agent Relay created for a claim
user_Anthropic account ID. Share it with Anthropic support.
cagt_Claude Tag agent ID, on sessions started by Claude Tag in Slack
anthropic:Anthropic service name. Values that start with anthropic:slack mark Claude Tag sessions.

Parse records

  • Ingest stderr as one JSON object per line. Route lines that aren't JSON to a fallback index instead of dropping them, because a plain agent-relay: ... startup failure might be the only sign that the service never started.
  • Use ts as the event time, not the ingestion time.
  • Flatten fields, and map IDs and UUIDs as keyword or string types.
  • Set field types explicitly:
    • Claude Code attempt is a string, not a number.
    • Cursor provider_user_id and user_id are numbers.
    • retryable is a boolean, and expected_spawn_seconds is a number.
    • deadline is an RFC 3339 timestamp.
    • Durations such as interval, retry_interval, relist_interval, and age are Go duration strings, such as 5s or 5m0s.
  • Treat empty strings as missing. On Claude Code records, account_id, account_email, agent_id, and org_service_name are present but empty when they don't apply.
  • Allow duplicate keys. Records written after Agent Relay creates a workspace can contain owner twice, and Claude Code nack records can contain attempt twice. Correlate users on owner_id, not owner.

Scope records to a provider

  • Select a provider's records by logger_names: claude-code-runners or cursor-workers. Some records carry no provider field, such as no claude code runner pools configured, claude provider disabled, so a filter on fields.provider alone misses them.
  • Also collect reaper records, and attribute them by workspace_name prefix: cc- for Claude Code and cur- for Cursor. Join them to a session on workspace_id.
  • Add fields.pool to the filter when one Agent Relay deployment serves more than one pool.

To measure user impact, count distinct session IDs, not records: session_uuid for Claude Code and session_id for Cursor. A retried session gets a new delivery_id under the same session ID, so one session can fail more than once.

Match rules

  • Match on msg and level. Do not match on caller or func, because file paths, line numbers, and function names change between builds.
  • msg text can change between Agent Relay releases. Recheck saved searches and alert rules after each upgrade.
  • Alert on specific msg values, not on every WARN record. Several WARN messages describe automatic recovery, such as poll failed, retrying next interval, identity resolution failed, nacking retryable, and claim failed, retrying next pass.

Log-based alerts

These alerts complement the suggested Prometheus alerts. Some conditions, such as a stranded Cursor claim, appear only in logs.

PrioritymsgProviderGroup byWhy
Pagefatal poll error, stopping this pool until agent-relay restarts with fixed config (ERROR)Claude CodepoolThe pool stops taking sessions until Agent Relay restarts with fixed configuration, often after an environment key expires or is rotated.
Pagefatal listing error, degrading the pool until verification passes (ERROR)CursorpoolThe pool stops claiming requests until verification passes.
Pagefail claim request failed, claim stranded until Cursor-side expiryCursorpoolThe user's chat waits with no error until Cursor expires the claim. Only the logs record this.
Pagenack failed, relying on TTL redelivery or nack not applied, session will keep redeliveringClaude CodepoolAnthropic didn't record the failure, so the session waits out its spawn budget or keeps redelivering.
Page when sustainedpool failed verification, retryingBothpool, templateA bad API key or environment key, a template without the Agent Relay module, or an organization problem. Alert after several records in a row.
Rate thresholdnacked spawn hintClaude Codepool, retryableSessions handed back to Anthropic. retryable: false ends the session and needs an administrator.
Rate thresholdfailed the claim upstream, the user sees the messageCursorpool, retryable, stageUsers saw a failure. retryable: false needs an administrator. retryable: true usually means a transient or capacity problem.
Rate thresholdworkspace build failed, reporting retryable and deletingBothjob_errorA template or infrastructure failure, such as exhausted quota. Agent Relay deletes the workspace right away, so job_error might be the only record of why.
Rate thresholdworkspace build passed the session deadline, reporting retryable and deletingBothpoolBuilds take longer than the 300-second Claude Code budget or the Cursor pool's dispatch_deadline.
Rate thresholdsession did not finish cleanly, reporting and deleting (reaper)BothreasonRunners that exited non-zero, couldn't start, stopped unexpectedly, or never started.
Ticketsession owner is unresolvable, nacking non-retryableClaude Codeorg_service_nameA Claude user has no active Coder account in the pool's organization, or a Claude Tag channel session arrived without claude_tag_channel_user.
Ticketrequest owner cannot be served, refusing the requestCursorreasonA Cursor request has no email, no matching Coder account in the organization, or a suspended or dormant account.
Ticketwork order failed verification, nacking non-retryableClaude CodepoolAgent Relay rejected the Anthropic-signed work order. error gives the reason.
Ticketworkspace delete keeps failing, leaving it for an operator (reaper)Bothworkspace_idThe reaper stopped retrying after repeated delete failures. Clean up the workspace by hand.
Ticketdelete workspace failed, leaving for reconciliation or delete workspace failed, retrying next sweepBothworkspace_idA workspace might still be running and using resources.
Dashboardresolved session owner, claimed pending request, workspace build created, workspace ready, runner handoff, or deleting workspaceBothpool, client_platformThe normal session lifecycle, for volume and funnel views.

Retention and privacy

  • Keep logs for as long as your audit requirements need. Agent Relay doesn't keep session history, so your log platform is the record of who ran which session and why it failed.
  • Use Prometheus metrics for rates and latency, and the SIEM for ordered event history, investigations, and the alerts above.
  • Records can contain Coder usernames and user IDs, Anthropic account IDs, and Cursor user IDs. Claude Code records carry account_email on every record after work order verification, and Cursor refusal records carry user_email.
  • Emails also appear inside error, detail, and job_error strings, such as identity warnings. Mask these fields, or restrict them with role-based access, along with the other user-identifying fields.

Learn more