Skip to content

ARC-1 Log Analysis Guide

Enabling File Logging

Set the ARC1_LOG_FILE environment variable to enable JSON line audit logging:

# Local development
ARC1_LOG_FILE=/tmp/arc1-audit.jsonl npm run dev

# Docker
docker run -v /data/logs:/logs -e ARC1_LOG_FILE=/logs/arc1-audit.jsonl ghcr.io/arc-mcp/arc-1

# BTP Cloud Foundry (in manifest.yml)
env:
  ARC1_LOG_FILE: /tmp/arc1-audit.jsonl

On BTP Cloud Foundry, ARC-1's stderr logs are always available via cf logs arc1-mcp-server (live) and cf logs arc1-mcp-server --recent (buffer) — no service binding required. The deprecated Application Logging Service (Kibana) is off by default (SAP Note 3557260); see BTP Cloud Foundry Deployment to opt back in, or use SAP Cloud Logging for a managed observability stack.

Log Levels

Control stderr verbosity with ARC1_LOG_LEVEL:

ARC1_LOG_LEVEL=debug  # Show everything (HTTP requests, CSRF fetches)
ARC1_LOG_LEVEL=info   # Default — tool calls, auth events
ARC1_LOG_LEVEL=warn   # Only warnings and errors
ARC1_LOG_LEVEL=error  # Only errors

The file sink always receives ALL events regardless of stderr level.

Event Types

The exact event inventory and field contract is maintained in Security Guide: What Gets Logged. Important operator groups include:

Group Events
Tool and SAP HTTP lifecycle tool_call_start, tool_call_end, http_request, http_csrf_fetch
Authorization and safety auth_scope_denied, safety_blocked, auth_rate_limited, mcp_rate_limited
Selected identity auth_pp_created, auth_shared_created
Multi-target failure stage target_resolution_failed, pp_exchange_failed, shared_auth_failed, cloud_connector_access_denied, sap_service_unavailable, sap_authentication_failed, sap_authorization_failed, target_policy_denied
Server/client protocol server_start, OAuth/DCR, and CORS events

Within a selected multi-target call, use requestId to correlate events and target, destination, and identity to identify the selected route and identity model. Failure-stage events also carry a safe errorCode; they do not contain destination credentials or SAP response bodies.

What a Healthy Startup Looks Like

After you deploy (or run locally), the startup transcript is the fastest way to confirm SAP connectivity and authorization are working — before you ever make a tool call. On BTP Cloud Foundry, read it with cf logs arc1-mcp-server --recent (or the Logs tab of the app in the BTP Cockpit).

A healthy startup at the default info level looks like this (real output, S/4HANA 2023 / ABAP Platform 2025):

INFO: [server_start] {"version":"0.9.x","transport":"stdio","allowWrites":...,"url":"http://your-sap:50000"}
INFO: ARC-1 starting {"version":"0.9.x","transport":"...","url":"..."}
INFO: SAP semaphore {"maxConcurrent":10,"scope":"server-wide"}
INFO: Object cache enabled {"mode":"auto",...}
INFO: ARC-1 MCP server running on stdio          # (or: "ARC-1 HTTP server started" on BTP)
INFO: Startup auth preflight succeeded for shared SAP credentials. {"endpoint":"/sap/bc/adt/core/discovery"}
INFO: Authorization probe: object search access is available
INFO: Authorization probe: transport access is available

The two green-light signals

INFO: Authorization probe: object search access is available
INFO: Authorization probe: transport access is available

These two lines mean your SAP authorizations are correct. If you see them, ARC-1 reached SAP, authenticated, and the SAP user can search the repository and read transports — the foundation every tool call builds on. (Under principal propagation the preflight is skipped — each user authenticates at runtime — so you'll instead see Skipped startup auth preflight: principal propagation mode is enabled; the per-user authorization probe then runs on that user's first call.)

If instead you see either of:

WARN: Authorization probe: object search access denied — <reason>
INFO: Authorization probe: transport access is not available — <reason>

…the SAP user is missing an authorization (not an ARC-1 bug). Search/read needs S_DEVELOP and S_ADT_RES (read-only users need S_ADT_RES with ACTVT = 01 AND 02 — several ADT reads are POSTs). See Authorization and Principal Propagation.

"Feature not available" is normal, not an error

ARC-1 probes optional capabilities at startup (abapGit, AMDP, RAP/CDS, UI5, HANA info, source search, …). Any capability your system doesn't have simply returns 404 (not installed / ICF service not active) or 400this is expected and is recorded as data, not an error. These probe misses are logged at debug, so they do not appear at the default info level. A clean startup has no WARN lines from probing.

If you run with ARC1_LOG_LEVEL=debug, you'll see them — and they're still harmless:

DEBUG: [http_request] {"method":"GET","path":"/sap/bc/adt/abapgit/repos","statusCode":404,...}
DEBUG: [http_request] {"method":"GET","path":"/sap/bc/adt/debugger/amdp","statusCode":404,...}
DEBUG: [http_request] {"method":"GET","path":"/sap/bc/adt/ddic/ddl/sources","statusCode":400,...}

These just mean abapGit/AMDP aren't installed and the RAP probe returned its expected 400 — ARC-1 disables those features gracefully and serves the rest. The resolved feature set is what matters, not the individual probe responses.

A genuine problem looks different: a WARN/error auth_scope_denied, a 401 on the auth preflight, an Authorization probe: … denied line, or Startup auth preflight failed — those are worth investigating; a 404 probe miss at debug is not.

OAuth scope errors on the MCP client (not SAP)

A different failure class: the MCP client (Claude, Copilot, …) can't complete OAuth and reports an invalid_scope / scope error even though your user has the right role collection. This is almost always a stale cache, not a missing authorization:

  • Log out of the MCP client's OAuth session and reconnect — or use a fresh/incognito browser window for the consent step. A previous deployment's XSUAA/DCR client registration is often cached.
  • Verify the role collection is assigned under the correct identity provider. If your subaccount uses a custom IdP (e.g. SAP IAS), assign the role collection to the user under that IdP (--of-idp <your-idp>), not the default SAP ID service — otherwise the JWT carries no ARC-1 scopes.
  • After a redeploy that recreated the XSUAA service, give the client one clean re-login; cached client_ids from the old service instance produce scope errors until they re-register.

Analyzing Logs with jq

Recent Errors

# All errors in the last hour
jq 'select(.level == "error")' arc1-audit.jsonl

# Failed tool calls with error details
jq 'select(.event == "tool_call_end" and .status == "error")' arc1-audit.jsonl

# Failed tool calls grouped by error class
jq -s '[.[] | select(.event == "tool_call_end" and .status == "error")] | group_by(.errorClass) | map({errorClass: .[0].errorClass, count: length})' arc1-audit.jsonl

Multi-Target Failures

# Every selected-target failure stage
jq 'select(.event as $e | ["target_resolution_failed", "pp_exchange_failed", "shared_auth_failed", "cloud_connector_access_denied", "sap_service_unavailable", "sap_authentication_failed", "sap_authorization_failed", "target_policy_denied"] | index($e))' arc1-audit.jsonl

# One public target across identity, policy, and SAP failure stages
jq 'select(.target == "A4H/100" and .errorCode? != null)' arc1-audit.jsonl

# Failure counts by safe error code
jq -s '[.[] | select(.errorCode? != null) | .errorCode] | group_by(.) | map({errorCode: .[0], count: length}) | sort_by(-.count)' arc1-audit.jsonl

# Successful shared-identity canaries, grouped by destination
jq -s '[.[] | select(.event == "auth_shared_created")] | group_by(.destination) | map({destination: .[0].destination, successes: length})' arc1-audit.jsonl

target is the public SID/client or alias/client ID, while destination is the internal BTP destination name. identity distinguishes per-user Principal Propagation from a shared Basic technical user. Correlate the failure-stage event with the same requestId's tool_call_end; do not expect raw SAP response bodies in these events.

Bad/Wrong Tool Calls (for improving LLM feedback)

# Tool calls that returned client-visible handler errors (unknown tool/action, validation, etc.)
jq 'select(.event == "tool_call_end" and .status == "error" and .errorClass == "result-path")' arc1-audit.jsonl

# Tool calls blocked by safety (LLM tried a blocked operation)
jq 'select(.event == "tool_call_end" and .errorClass == "AdtSafetyError")' arc1-audit.jsonl

# Auth scope denials (LLM called a tool the user can't access)
jq 'select(.event == "auth_scope_denied")' arc1-audit.jsonl

# Error counts by class — errorMessage content is redacted before sink writes
jq -s '[.[] | select(.event == "tool_call_end" and .status == "error") | .errorClass] | group_by(.) | map({errorClass: .[0], count: length}) | sort_by(-.count)' arc1-audit.jsonl

Slow Operations

# Tool calls taking >5 seconds
jq 'select(.event == "tool_call_end" and .durationMs > 5000)' arc1-audit.jsonl

# HTTP requests taking >10 seconds
jq 'select(.event == "http_request" and .durationMs > 10000)' arc1-audit.jsonl

# Average duration by tool
jq -s '[.[] | select(.event == "tool_call_end")] | group_by(.tool) | map({tool: .[0].tool, avgMs: (map(.durationMs) | add / length | round), count: length})' arc1-audit.jsonl

Correlating Events by Request ID

Every tool call generates a unique requestId (e.g., REQ-42). All HTTP requests made during that tool call share the same ID:

# Trace a specific tool call through all its HTTP requests
jq 'select(.requestId == "REQ-42")' arc1-audit.jsonl

# Find tool calls that made many HTTP requests (potential performance issue)
jq -s '[.[] | select(.event == "http_request")] | group_by(.requestId) | map({requestId: .[0].requestId, httpCalls: length}) | sort_by(-.httpCalls) | .[:10]' arc1-audit.jsonl

HTTP-Level Analysis

# Failed HTTP requests (4xx/5xx)
jq 'select(.event == "http_request" and .statusCode >= 400)' arc1-audit.jsonl

# Non-authentication failures for which a redacted error-body placeholder was retained
jq 'select(.event == "http_request" and .errorBody != null)' arc1-audit.jsonl

# Most common ADT paths called
jq -s '[.[] | select(.event == "http_request") | .path] | group_by(.) | map({path: .[0], count: length}) | sort_by(-.count) | .[:10]' arc1-audit.jsonl

The errorBody query deliberately excludes SAP HTTP 401 and 403 responses. Those bodies can expose technical usernames, echoed login material, or SAP security details, so ARC-1 omits them even when ARC1_LOG_HTTP_DEBUG=true. Use the status code, selected-target failure event, errorCode, and requestId instead. Other error bodies are replaced with a length-only redacted placeholder before sink writes; their content is never present in the audit file.

User Activity

# Tool calls per user
jq -s '[.[] | select(.event == "tool_call_start" and .user != null)] | group_by(.user) | map({user: .[0].user, calls: length})' arc1-audit.jsonl

# What tools a specific user called
jq 'select(.event == "tool_call_start" and .user == "john.doe@company.com")' arc1-audit.jsonl

BTP Audit Log Service

When deployed on BTP with the Audit Log Service premium plan bound, ARC-1 automatically forwards categorized security and tool-call events to the BTP Audit Log Viewer. Low-level HTTP, startup, and elicitation events remain in stderr/file logs. Forwarded events are categorized as:

  • security-events: auth/target/service failures, scope denials, safety blocks, shared-identity use
  • data-accesses: tool calls that read SAP data (SAPRead, SAPSearch, SAPQuery)
  • data-modifications: tool calls that write data (SAPWrite, SAPManage)
  • configuration-changes: transport and activation operations (SAPTransport, SAPActivate)

View these in the BTP cockpit under Instances and Subscriptions > Audit Log Viewer.

Docker Volume Mount Example

# Run with persistent log file
docker run -d \
  -v /data/arc1-logs:/logs \
  -e ARC1_LOG_FILE=/logs/audit.jsonl \
  -e SAP_URL=http://sap:50000 \
  -e SAP_USER=admin \
  -e SAP_PASSWORD=secret \
  ghcr.io/arc-mcp/arc-1

# Tail logs in real-time
tail -f /data/arc1-logs/audit.jsonl | jq .

# Watch for errors only
tail -f /data/arc1-logs/audit.jsonl | jq 'select(.level == "error")'