Skip to content

Proxy OTLP API

The proxy OTLP API is the wire contract between the Softprobe proxy (Envoy + WASM) and softprobe-runtime. In the default OSS layout this API is served by the same process as the HTTP control API, sharing one in-memory session store.

You only need this page if you are:

  • building or auditing a custom proxy integration,
  • debugging inject/extract traffic at the OTLP layer, or
  • implementing a hosted backend (like runtime.softprobe.dev) against the same contract.

Most users interact with the runtime through SDKs and the CLI. Tests never call these endpoints directly.

The normative source is spec/protocol/proxy-otel-api.md. This page summarizes it in user-oriented form.

Transport

  • HTTP/1.1 or HTTP/2 over TCP (no streaming; request/response only).
  • Payloads are OTLP TracesData — the same envelope standard OpenTelemetry SDKs produce.
  • Content-Type: application/x-protobuf or application/json.
  • Accept: application/x-protobuf or application/json on endpoints that return OTLP payloads.

The OSS proxy (Rust/WASM reference) negotiates protobuf by default; JSON is supported for ease of debugging with curl and for third-party proxies.

Endpoints

There are exactly two:

MethodPathPurposeBlocking?
POST/v1/injectRequest-path lookup: should this outbound be mocked, failed, or forwarded?Yes — on the hot path of every intercepted HTTP hop
POST/v1/tracesAsync upload of observed (passthrough) traffic for later replayNo — fire-and-forget from the proxy's POV

Both endpoints share the same OTLP envelope, differing only in the sp.span.type attribute and the server's response semantics.

POST /v1/inject

Called on every intercepted HTTP hop (ingress or egress) that carries a Softprobe session. The proxy builds an OTLP trace describing the candidate exchange and waits for the runtime's decision.

Request body

OTLP TracesData containing one inject span. Key attributes:

AttributeRequiredPurpose
sp.span.typeyesMust be "inject"
sp.session.idyesSession the request belongs to (from x-softprobe-session-id via tracestate)
sp.traffic.directionyes"inbound" or "outbound"
sp.service.nameyesLogical name of the service the proxy is attached to
url.hostyesTarget host (outbound) or listener host (inbound)
url.pathyesRequest path
http.request.methodyesGET, POST, etc.
http.request.header.<name>noPer-header values
http.request.bodynoRequest body, UTF-8 string (or base64 for binary)

Response

StatusMeaningProxy behavior
200 OK + OTLP bodyHit — a rule matched, use the supplied responseSynthesize HTTP response from http.response.* attributes; do not call upstream
404 Not FoundMiss — no rule matchedForward the request to the real upstream (normal proxying)
5xx / timeoutError from the runtimeApply local fallback per proxy config; strict mode returns 5xx to the caller

Response body on hit

The returned OTLP span carries:

AttributeRequiredPurpose
http.response.status_codeyesNumeric status (200, 401, 503, …)
http.response.header.<name>noPer-header values
http.response.bodynoResponse body, UTF-8 string (or base64 for binary)

The proxy ignores span identity (traceId, spanId, timestamps) on the response — only attributes matter.

Worked request example

json
{
  "resourceSpans": [
    {
      "resource": {
        "attributes": [
          { "key": "sp.service.name", "value": { "stringValue": "checkout" } }
        ]
      },
      "scopeSpans": [
        {
          "spans": [
            {
              "traceId": "0af7651916cd43dd8448eb211c80319c",
              "spanId":  "b7ad6b7169203331",
              "name": "HTTP POST",
              "kind": 3,
              "attributes": [
                { "key": "sp.span.type",          "value": { "stringValue": "inject" } },
                { "key": "sp.session.id",         "value": { "stringValue": "sess_01H..." } },
                { "key": "sp.traffic.direction",  "value": { "stringValue": "outbound" } },
                { "key": "url.host",              "value": { "stringValue": "api.stripe.com" } },
                { "key": "url.path",              "value": { "stringValue": "/v1/payment_intents" } },
                { "key": "http.request.method",   "value": { "stringValue": "POST" } },
                { "key": "http.request.body",     "value": { "stringValue": "amount=1000&currency=usd" } }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Worked hit response

json
{
  "resourceSpans": [
    {
      "scopeSpans": [
        {
          "spans": [
            {
              "attributes": [
                { "key": "http.response.status_code",      "value": { "intValue": "200" } },
                { "key": "http.response.header.content-type", "value": { "stringValue": "application/json" } },
                { "key": "http.response.body",             "value": { "stringValue": "{\"id\":\"pi_test\",\"status\":\"succeeded\"}" } }
              ]
            }
          ]
        }
      ]
    }
  ]
}

POST /v1/traces

Called after the proxy forwards a passthrough request and has the real upstream's response. The proxy uploads the full request/response pair so the runtime can record it (capture mode) or ignore it (replay mode, depending on policy).

The request body uses the standard OTLP TracesData shape. In production the proxy sends these payloads out-of-band to sp_backend_url (the Softprobe runtime), not into your existing vendor APM pipeline by default — large http.*.body attributes would be truncated or rejected there. See Proxy integration posture. (You may optionally tee a filtered copy to a collector in advanced setups; that is not the default install path.)

Request body

OTLP TracesData containing one or more extract spans. Key attributes:

AttributeRequiredPurpose
sp.span.typeyesMust be "extract"
sp.session.idyesSession that owns this observation
sp.traffic.directionyes"inbound" or "outbound"
sp.service.nameyesService name
url.host, url.pathyesRequest target
http.request.methodyesHTTP method
http.request.header.<name>, http.request.bodynoRequest details
http.response.status_codeyesObserved upstream status
http.response.header.<name>, http.response.bodynoResponse details

Response

StatusMeaning
2xxAccepted; runtime will include in capture output
4xxRejected (unknown session, schema violation) — proxy logs and drops
5xxTransient failure — proxy may retry with exponential backoff within a bounded deadline

The proxy treats this endpoint as fire-and-forget: the outbound HTTP response to the original caller has already been sent when extract is uploaded. Extract upload must not extend request latency beyond the configured deadline.

Session correlation

Session identity flows through the proxy on every hop via W3C Trace Context, not through a custom HTTP header:

  1. The test sends x-softprobe-session-id: <id> on the inbound request.
  2. The ingress proxy reads that header, encodes the session id into tracestate (per session-headers.md), and forwards traceparent / tracestate to the app.
  3. The app propagates both headers on outbound calls via OpenTelemetry.
  4. The egress proxy reads tracestate, decodes the session id, and puts it in sp.session.id on every /v1/inject or /v1/traces call.

This means the runtime only ever sees session id via OTLP attribute — it never parses x-softprobe-session-id directly.

Error handling

The runtime returns machine-readable errors per spec/schemas/session-error.response.schema.json. Common cases:

ConditionStatusProxy behavior
Unknown / closed sp.session.id404Forward upstream (same as "no matching rule")
Malformed OTLP body400Log + drop; proxy should emit a local telemetry error
Runtime overload / circuit-breaker503Apply local fallback per proxy config
Timeout exceeding proxy deadlinen/aProxy aborts its own call and applies local fallback

Performance targets

Recommended SLOs for the inject path (measured at the runtime, not the proxy):

MetricTarget
p50 /v1/inject latency< 1 ms
p99 /v1/inject latency< 5 ms
Max inject throughput~ 20k rps / CPU core (in-memory store, single process)

The extract path has no hard latency SLO since it's async; aim for < 100 ms p99 end-to-end to keep capture buffers small.

Observability

Runtime implementations should expose the following (not normative, but strongly recommended):

  • Prometheus metrics: softprobe_inject_requests_total{outcome="hit|miss|error"}, softprobe_inject_latency_seconds, softprobe_extract_bytes_total.
  • Structured logs with sp.session.id for correlation.
  • An /health endpoint for proxy liveness checks.

See also

  • HTTP control API — tests and CLI write rules via the control API; the proxy reads them via OTLP.
  • Case file schema — extract spans become case file entries.
  • Session headersx-softprobe-session-id and its relationship to tracestate.
  • Rule schema — what the runtime evaluates on each /v1/inject.
  • Architecture — how control plane and data plane share one process.