Skip to content

Where the agent sends data

The senders stage of a collection config, and every destination the shipped agent actually supports.


A collection config is readers → transformers → senders. A sender names its destination and carries that destination’s settings:

senders:
- sondar:
repo: app_logs
sourcetype: json
url:
- https://sondar.internal:9200
token: "<collect token>"

More than one sender can be listed; each receives every event the readers produce, after transformers. That is the supported way to fan out — send the same stream to Sondar and to a Kafka topic, for example.


name what it is
sondar log and metric events into an index. The one you want almost always.
sondar_event discrete events rather than log lines
sondar_topo topology relationships

sondar needs repo (the index), sourcetype, one or more url, and a token. Get the token wrong and the agent retries with response code: 401 … Access denied, unauthenticated in its own log and nothing anywhere else — no data arrives and the server never learns why, so check the agent log first when an index stays empty.

name notes
elasticsearch for teams keeping an existing ES cluster in the path
kafka the usual choice for fanning out to other consumers
http POST to an arbitrary endpoint
syslog forward as syslog
opentelemetry OTLP. Sondar’s own APM ingest is OTLP on 4317
openmetrics expose metrics for scraping rather than pushing
name notes
s3 S3-compatible object storage
file a local file
archive_local, archive_s3, archive_sftp the archive family — write-once copies for retention and compliance

If you have a “keep an immutable copy for N years” requirement, this is the mechanism — worth knowing it exists before designing something else.

One sender per engine: mysql, postgres, oracle, mssql, dm.

Each needs a table_name; omit it and the runner refuses to start with load config failed: table_name is a required field. Writing logs to a relational database is rarely the right call for volume, but it is the usual answer when a downstream report has to join against existing tables.

discard throws events away, example and test exist for the agent’s own tests. Useful when you want to run a config for its readers and transformers without a destination — discard is the honest way to do that.


A sender name the agent does not know is not a loud failure. Before August 2026 the shipped collection configs named a sender the agent lacked, and the server’s resolution of that config silently returned nothing. Both spellings are now accepted and the agent keeps a deprecated alias, but the lesson stands: if an index is empty, confirm the sender name is one of the above before looking anywhere else.

Config errors are reported by the agent, not the UI. A missing required field stops the runner loading and is written to the agent’s log with the reason. As of August 2026 those messages are English; earlier builds emitted them in Chinese.

Oversized lines are handled differently at each end. The agent drops a line over max_line_len (1 MB by default) and logs a warning; the server truncates at 100,000 characters silently. A line between the two arrives cut in half with no indication. See indexes-and-field-extraction.md.


The sondar sender is the default destination and the one with the most moving parts. Verified defaults: gzip: true, escape_html: true, repo: default, non_retryable_statuscodes: [400]. HTTP tuning nests under client: (timeouts, connection pools, TLS, proxy, oauth — see the shared HTTP config below).

senders:
- sondar:
repo: app_logs
sourcetype: json
url:
- https://sondar.internal:9200
token: "<collect token>"
gzip: true
escape_html: true
client:
timeout: 30s

Why the sender also answers to keta. The shipped collection configs and every config written against the earlier product open the senders block with - keta:, and the server still reads that key to work out which index a config targets. The agent therefore registers the old name as a deprecated alias — sender.New would otherwise fail with sender type unsupported : keta and the entire integration catalog would be dead. Do not use keta: in a new config, and do not “clean up” the alias in the agent: retiring it requires the server’s KEY_TYPE_KETA resolution to move in the same change, or configs stop resolving their target index silently.

The client: block on sondar and http senders carries the same options: timeout, idle_conn_timeout, max_idle_conns, max_idle_conns_per_host, max_conns_per_host, plus inline proxy, TLS, OAuth2 and cookie-auth config. It is the same httpcommon.ClientConfig both senders embed.


Each destination below was verified against the sender’s types.go; the required fields are marked. serialization.type (where offered) defaults to json; use_batch_format toggles a batched (list) envelope for file and HTTP output.

senders:
- elasticsearch:
urls: ["http://localhost:9200"]
index: "app_logs" # required
indexname_time_field: "@timestamp" # route docs to date-suffixed indexes
indexname_timezone: "UTC"
username: elastic
password: changeme
timeout: 30s

index is required; indexname_time_field + indexname_timezone switch on time-based index names (e.g. app_logs-2026.08.13) instead of a fixed index. enable_gzip, enable_sniffer, id_field (the document _id), es_type and the connection pool keys (timeout, idle_conn_timeout, max_idle_conn) complete the set.

senders:
- kafka:
brokers: ["127.0.0.1:9092"] # required
topic: app_logs # required
partition_type: hash # hash (default) or robin
topic_tag: topic # take the topic from an event field
serialization:
type: json

brokers and topic are required. partition_type is hash by default (robin for round-robin); topic_tag lets the topic come from a field on each event. SASL and TLS ride on the inline KafkaConfig.

senders:
- http:
urls: ["https://collector.example/api"]
method: POST
username: user
password: secret
headers:
X-Team: ops
content_encoding: gzip
use_batch_format: false
client:
timeout: 30s

Defaults: content type application/json, content encoding gzip. The url singular key is deprecated in favour of urls. A non_retryable_statuscodes list keeps the agent from retrying responses that will never succeed.

senders:
- syslog:
address: "logs.example:514" # UDP by default
appname: myapp
severity_code: 6 # informational
facility_code: 1
framing: true

address is required. The event’s own fields override these defaults: the sender looks for version, severity_code, facility_code, proc_id, msg_id, msg, timestamp, sdid, hostname, source, host and appname on the event and uses them if present. Structured data rides in sdid/sdids (with separator for the @-style structured-data keys) and framing/delimiter control the message trailer. TLS config is inline.

senders:
- file:
path: /var/spool/sondar/out.log # required
use_batch_format: false
serialization:
type: json
- s3:
endpoint: https://s3.example
region: us-east-1
bucket: archive
access_key_id: AKIA...
secret_access_key: "..."
rolling_max_count: 10000
rolling_max_size: 1GB
rolling_max_duration: 24h
compress_type: gzip

The s3 sender rolls by count/size/duration (all three have bounds), compresses gzip by default, optionally writes an md5 sidecar (md5_check) and a timestamp_field. file just writes lines to path.

archive_local, archive_s3 and archive_sftp are write-once copies for retention and compliance — distinct from the file/s3 senders, which keep writing to the same target. Shared config: field (the file-path field, default origin), target_dir (required), target_file_path (a template; empty = keep the original file name), compress (gzip), keep_dir, md5_check, delete_after_archive.

senders:
- archive_local:
target_dir: /srv/retention/logs
compress: true
delete_after_archive: false
- archive_s3:
target_dir: logs/raw
s3_endpoint: https://s3.example # required
s3_region: us-east-1 # required
s3_access_key_id: AKIA... # required
s3_secret_access_key: "..." # required
s3_bucket: retention # required
s3_use_path_style: true
s3_server_side_encryption: AES256
- archive_sftp:
target_dir: /srv/retention
sftp_address: "backup.example:22" # required
sftp_user: backup # required
sftp_private_key_file: /etc/agent/id_ed25519

Each of mysql, postgres, oracle, mssql, dm needs table_name and dsn, plus the shared database options (max_open_conns default 10, etc.). The sender maps the event’s columns onto the table’s schema and inserts; it reads the table’s column list on start (GetColumnSchemas).

senders:
- mysql:
table_name: app_logs # required
dsn: "<user>:<password>@tcp(localhost:3306)/logs?parseTime=true"
max_open_conns: 10

Writing logs to a relational database is rarely the right call for volume, but it is the usual answer when a downstream report has to join against existing tables.

senders:
- opentelemetry:
endpoint: "http://localhost:4317"
traces_enabled: true
metrics_enabled: true
logs_enabled: true
connect_timeout: 30s

endpoint/endpoints, the signal toggles, metadata (header pairs) and the inline gRPC/TLS tuning. Sondar’s own APM ingest is OTLP on 4317, so this is how an agent forwards traces/metrics/logs to another collector.


The openmetrics sender is the one sender that does not push. It converts each event to the OpenMetrics text format and serves it over HTTP for a scraper (Prometheus) to collect.

senders:
- openmetrics:
urls: ["http://0.0.0.0:8899/metrics"]
headers:
Authorization: "Bearer ..."
max_idle_conns: 3
timeout: 5s

The conversion requires each event to be shaped a particular way — an event that does not have all three is logged and dropped:

  • fields — the metric values. Each key becomes a metric family; a key ending _histogram emits the cumulative le-bucketed histogram lines.
  • tags — the labels, rendered as key="value" in the braces. Boolean values become 1/0.
  • timestamp — the metric timestamp. Omitted when zero.

A rendered line looks like this (what a scraper actually receives):

# HELP cpu_usage type metrics for module system_tel
# TYPE cpu_usage gauge
cpu_usage{host="db-1",env="prod"} 63.2 1723500000
# HELP http_requests_total type metrics for module apache
# TYPE http_requests_total counter
http_requests_total{method="GET"} 42 1723500000
# EOF

The # TYPE line is derived per metric family, booleans render as 1/0, and the sample ends with the 10-digit Unix timestamp. A batch that produces no bytes makes the endpoint answer send 0 bytes to collector — the agent logs that string as a warning, which is the signal that the upstream events were not shaped with fields/tags/timestamp.

urls is required (this sender uses the same config shape as http, with headers, max_idle_conns and timeout). The listener is the collector’s pull side; push happens only when the scraper hits the URL.