Docs: Splunk HEC
Documentation / Ingestion

Splunk HEC

Send events to OBSESC with the Splunk HTTP Event Collector protocol, including JSON, raw and useACK clients.

OBSESC implements the Splunk HTTP Event Collector (HEC) protocol, so any tool with a HEC output can add OBSESC as a second destination. You keep the same token scheme and payload format. OBSESC accepts HEC only. It doesn’t receive the Splunk forwarder-to-indexer protocol, so a forwarder must send to OBSESC through a HEC output.

Endpoints

Method and pathPurpose
POST /services/collectorJSON events (same as /event)
POST /services/collector/eventJSON events: one object, newline-separated objects, or back-to-back objects
POST /services/collector/rawRaw text. Each non-empty line is one event
GET /services/collector/healthHealth probe. Returns {"text":"HEC is healthy","code":17}, no token needed
POST /services/collector/ackAcknowledgement status for useACK clients

The default port is 8088 (ingest.hec_port). The stock CloudFormation stack doesn’t open 8088, so add an inbound security group rule first. See Network configuration.

Request bodies can be up to 16 MiB by default (ingest.hec_max_body_bytes). Content-Encoding: gzip is accepted.

Configure tokens first

The HEC listener fails closed. Until you configure at least one token, it rejects every request with 403 Invalid token.

security:
  hec_tokens:
    - "<your-hec-token>"

You can also put hec_tokens (a JSON array) in the node’s Secrets Manager secret. See Configuration examples. Clients send Authorization: Splunk <token>, and the token must match exactly.

Quick test

curl -sS -X POST "http://obsesc.your-domain.internal:8088/services/collector/event" \
  -H "Authorization: Splunk $OBSESC_HEC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"time": 1767225600.123, "sourcetype": "checkout", "host": "web-1", "event": "order placed id=42"}'

A successful request returns {"text":"Success","code":0}.

Client configurations

OpenTelemetry Collector (splunk_hec exporter)

exporters:
  splunk_hec/obsesc:
    endpoint: http://obsesc.your-domain.internal:8088/services/collector
    token: ${env:OBSESC_HEC_TOKEN}
    sourcetype: checkout

For the Collector, OTLP is usually the better choice, because it keeps resource attributes and also carries traces.

Fluent Bit (splunk output)

[OUTPUT]
    Name          splunk
    Match         *
    Host          obsesc.your-domain.internal
    Port          8088
    Splunk_Token  ${OBSESC_HEC_TOKEN}
    TLS           Off

Vector (splunk_hec_logs sink)

[sinks.obsesc_hec]
type = "splunk_hec_logs"
inputs = ["app_logs"]
endpoint = "http://obsesc.your-domain.internal:8088"
default_token = "${OBSESC_HEC_TOKEN}"
sourcetype = "checkout"
encoding.codec = "text"

Cribl Stream

Add a Splunk HEC destination with the endpoint http://obsesc.your-domain.internal:8088/services/collector/event and a token from security.hec_tokens. Then add a route or a Clone function so the same stream also goes to OBSESC.

How events are mapped

JSON endpoints

HEC fieldOBSESC field
sourcetype, else source, else noneservice. With neither, the event goes to ingest.default_service
event (string)body, as sent
event (object)body holds the full JSON, and its fields are also flattened into dotted attributes
time (epoch seconds, fractional allowed, number or string)timestamp. If it’s missing, the node’s receive time is used
host, indexhost and index attributes
source (when it isn’t already the service)source attribute
fieldsOne attribute per key

Every event must have an event field. A batch where any event is missing it, or any event isn’t valid JSON, is rejected whole with code 6. Nothing from that batch is stored, so the forwarder can safely resend it.

Raw endpoint

/services/collector/raw takes its metadata from the query string, not the body:

curl -sS -X POST \
  "http://obsesc.your-domain.internal:8088/services/collector/raw?sourcetype=checkout&host=web-1" \
  -H "Authorization: Splunk $OBSESC_HEC_TOKEN" \
  --data-binary @app.log

Each non-empty line becomes one event, with trailing \r removed. Raw events have no per-line timestamp, so they are stamped with the time the node received them.

Indexer acknowledgement (useACK)

Clients that send the X-Splunk-Request-Channel header get an ackId in each success response:

{"text":"Success","code":0,"ackId":7}

The client then polls POST /services/collector/ack with the same channel header and a body of {"acks":[7]}. An ackId is only issued after the batch is durable. A request to /ack without the channel header gets code 10, Data channel is missing.

Response codes

HTTPHEC codeMeaning
2000Success. The batch is durable
4012Token is required: no token was sent
4034Invalid token: the token isn’t in hec_tokens, or no tokens are configured
4006Invalid data format: malformed JSON, missing event, or a bad gzip body
413n/aBody, or decompressed body, is over the limit
415n/aContent-Encoding other than gzip or identity
5039Server is busy: backpressure or draining. Retry with backoff

Delivery and retries

  • JSON events that carry an explicit time are protected from duplicates: if a forwarder resends the identical batch soon after the original, it’s stored once.
  • Events without time, and everything sent to /raw, are at-least-once. A resent batch can be stored twice.

Multiline on the raw endpoint

/raw is line-oriented, so a multi-line stack trace arrives as several events. If the forwarder can’t assemble multiline events itself, turn on node-side assembly with ingest.multiline.enabled: true. See Configuration examples.