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.


namewhat it is
sondarlog and metric events into an index. The one you want almost always.
sondar_eventretired: it posted to an incident service Sondar does not ship. Alert apps use sondar (Alerts from other tools)
sondar_topotopology 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.

namenotes
elasticsearchfor teams keeping an existing ES cluster in the path
kafkathe usual choice for fanning out to other consumers
httpPOST to an arbitrary endpoint
syslogforward as syslog
opentelemetryOTLP. Sondar’s own APM ingest is OTLP on 4317
openmetricsexpose metrics for scraping rather than pushing
namenotes
s3S3-compatible object storage
filea local file
archive_local, archive_s3, archive_sftpthe 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. The agent now registers one sender name, sondar, and no legacy alias (see below) — so 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 1 MB (sondar.text_parser.max_line_bytes) and logs a rate-limited WARN naming the bytes discarded and the running total. Both thresholds are 1 MB, so a line the agent accepts is one the server accepts — a line above 1 MB never arrives at all, because the agent drops it. The server limit is measured in bytes rather than characters and is rounded down, so a multi-byte character is never split. See indexes-and-field-extraction.md.

Older notes describe a 100,000-character server limit that truncated silently, leaving a line between the two thresholds cut in half with no indication. That was sondar.text_parser.max_line_chars and it no longer applies.


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

On older examples that open the block with a different sender name. The agent used to register a second, legacy name so that configs written against the earlier product kept resolving. It was retired on 22 Aug 2026, together with the server-side key that resolved it and the last shipped configs that used it — the product now ships one sender name and no legacy alias, and both sides are held there by tests. Use sondar:. A config carrying the old name will not resolve.

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 favor 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.