axonpush
Concepts

OpenTelemetry over OTLP

OTLP/HTTP ingest in both JSON and protobuf, how a request is routed to a channel without a channel header, and how to forward the same events back out to a downstream collector.

axonpush speaks OTLP/HTTP in both directions. Anything that already exports OTLP, a Collector, a language SDK’s otlphttp exporter, a sidecar, can point at axonpush without changing application code, and anything axonpush stores can be forwarded on to a downstream OTLP endpoint the same way.

In: the ingest endpoints

MethodPathBody
POST/v1/logsOTLP ExportLogsServiceRequest, must contain resourceLogs[]
POST/v1/tracesOTLP ExportTraceServiceRequest, must contain resourceSpans[]

Send Content-Type: application/json for OTLP/JSON, or application/x-protobuf for the binary encoding. Both are decoded natively; neither is a second-class path.

A body over 6 MB is rejected with 413 before anything is parsed. The check reads Content-Length first and the decoded buffer second, so a lying Content-Length does not get you past it.

Headers

HeaderRequiredMeaning
X-API-Key: ak_…one of theseA server-side API key. Needs the publish scope.
X-Public-Token: pt_…one of theseA browser-safe token. Always requires X-Axonpush-Channel.
X-Axonpush-ChannelconditionalChannel ID to write into. See routing below.
X-Axonpush-EnvironmentnoEnvironment slug override. See Environments.
Idempotency-KeynoDeduplicates a retried export.

Channel routing

X-Axonpush-Channel carries a channel ID, an opaque string. The header is optional, and what happens when you omit it depends on the credential:

  • An app-scoped API key (a key with an appId) auto-routes. /v1/logs goes to a channel named otlp-logs and /v1/traces to one named otlp-traces, both inside the key’s app, both created on first use. This is the path of least friction: mint an app-scoped key, set two environment variables on your collector, and you are done.
  • An API key with no app gets 400, there is nothing to route into. Pin the key to an app, or send the header.
  • A public ingest token always gets 400. Tokens are scoped to one channel and must name it explicitly.

Response

200 with an empty object when everything was stored:

{}

When the batch could not be written, the OTLP partial-success shape comes back instead, rejectedLogRecords on /v1/logs, rejectedSpans on /v1/traces:

{
  "partialSuccess": {
    "rejectedLogRecords": 3,
    "errorMessage": "…"
  }
}

Two response headers report how the environment was resolved, which is the fastest way to confirm from curl that your export landed where you meant:

x-axonpush-resolved-environment: prod
x-axonpush-resolved-via: apiKey

Errors

StatusCause
400Body is missing resourceLogs[] / resourceSpans[], empty protobuf body, unroutable credential, or an environment override the key is not allowed to make.
401Missing or invalid credential.
403The credential does not own the channel in X-Axonpush-Channel, or lacks the publish scope.
404No such channel.
413Body over 6 MB. Lower your collector’s batch size.
429Ingest quota exhausted.

Collector configuration

The otlphttp exporter appends /v1/logs and /v1/traces itself, so the endpoint is the bare host:

exporters:
  otlphttp/axonpush:
    endpoint: https://api.axonpush.xyz
    encoding: json
    headers:
      X-API-Key: ${env:AXONPUSH_API_KEY}
      X-Axonpush-Environment: prod

service:
  pipelines:
    logs:
      receivers: [otlp]
      exporters: [otlphttp/axonpush]
    traces:
      receivers: [otlp]
      exporters: [otlphttp/axonpush]

Add X-Axonpush-Channel if your key is not app-scoped, or if you want both signals in one named channel rather than the two auto-created ones.

Straight from a language SDK

No axonpush package involved, this is the stock OpenTelemetry exporter:

import os

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

exporter = OTLPSpanExporter(
  endpoint="https://api.axonpush.xyz/v1/traces",
  headers={"X-API-Key": os.environ["AXONPUSH_API_KEY"]},
)

provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(exporter))

Out: export destinations

An export destination forwards every matching event back out over OTLP/HTTP, so axonpush can sit alongside a Datadog, Honeycomb or Grafana pipeline rather than replacing it. Destinations are scoped to one environment, which is how you forward production without also forwarding dev.

MethodPath
GET/export-destinations?envSlug=prod
POST/export-destinations
GET/export-destinations/{id}
PATCH/export-destinations/{id}
DELETE/export-destinations/{id}
curl -X POST https://api.axonpush.xyz/export-destinations \
  -H "Authorization: Bearer $AXONPUSH_JWT" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "datadog-prod",
        "envSlug": "prod",
        "endpointUrl": "https://otlp.datadoghq.com",
        "headers": { "DD-API-KEY": "…" },
        "signals": ["logs", "traces"],
        "serviceName": "checkout-api"
      }'
FieldNotes
endpointUrlAn OTLP/HTTP base URL. /v1/logs and /v1/traces are appended. Private and loopback addresses are rejected.
headersHeader map for credentials. Stored server-side and never returned by any read endpoint.
signalsAt least one of logs, traces.
eventTypeFilterAllow-list of event types. Empty or absent means all.
serviceNameStamped as service.name on the exported resource.
activeDefaults to true. DELETE deactivates rather than erasing.

Signal mapping is worth being precise about: every matching event is exported as an OTLP log when the destination subscribes to logs. Additionally, events of type app.span are exported as OTLP spans when the destination subscribes to traces. Subscribing to traces alone therefore forwards spans only.

Delivery is fire-and-forget with respect to ingest, a destination that is down never fails or slows the write that triggered it. In a cloud deployment deliveries are queued and retried by a worker before landing in a dead-letter queue; in a self-host or local deployment there is no worker process, so delivery happens inline and a failure is logged rather than retried.

Create, update and delete are recorded in the audit log as export_destination.created, .updated and .deleted.

OTLP or an SDK?

The OTLP endpoints are the right answer for a service that is already instrumented, you get axonpush without touching application code, and you keep the ability to leave by changing one exporter block.

The SDKs exist for the part OpenTelemetry does not model: agent semantics. agent.tool_call.start, agent.handoff and agent.llm.token are axonpush event types with dashboard and analytics meaning behind them, and the framework integrations emit them for you. Both routes end in the same store and the same trace, so mixing them is normal, OTLP from your HTTP and database layers, an SDK integration around the agent.

The AxonPushSpanExporter in each SDK writes through the events API rather than /v1/traces, so it carries agent attributes the raw OTLP path has no field for. If you only have spans, the raw endpoint is simpler.