Once an Envelope is flushed, xtcp2 serializes it with a chosen marshaller and sends it to a chosen destination. Marshallers control the wire format; destinations control where the bytes go. Destinations that need a heavy client library are gated behind build tags, so a binary only carries the backends it was compiled with.
- Marshallers
- The destination registry
- Destinations
- Kafka and the schema registry
- S3 and Parquet
- The record schema
- Quick recipes
- Configuration
- See also
pkg/xtcp/marshallers.go registers the available output formats, selected with -marshal:
| Value | Format | Use |
|---|---|---|
protobufList |
Length-delimited protobuf Envelopes | Production; what Kafka → ClickHouse consumes. |
protoJson |
Protobuf JSON (one object per Envelope) | Human-readable debugging. |
protoText |
Protobuf text | Human-readable debugging. |
msgpack |
MessagePack | Alternative compact debug format. |
jsonl |
JSON Lines / NDJSON (one record per line) | Pipe to jq, ingest into ClickHouse JSONEachRow, Loki, Vector. |
csv |
Comma-separated, header once, humanized | R / pandas / DuckDB / Excel. |
tsv |
Tab-separated, header once, humanized | Same, awk/cut-friendly. |
The protobufList format is the production wire format: it frames each Envelope as a length-delimited protobuf message, which ClickHouse's ProtobufList input format reads directly. The format and its rationale are covered in depth in protobufList migration.
Tabular formats (csv/tsv). Columns are generated from the XtcpFlatRecord protobuf descriptor via reflection, so they always match the schema. By default every field is emitted; restrict and order them with -columns (a comma-separated list of the camelCase json field names, e.g. -columns hostname,inetDiagMsgSocketSource,inetDiagMsgState,tcpInfoRtt). The header is written once per process.
Humanizing. csv and tsv render machine values human-readably: IP addresses as dotted-quad / RFC-5952 (the kernel returns them as raw bytes), the congestion enum as its short name (CUBIC, BBR3), TCP state as a name (LISTEN, ESTABLISHED), and timestamp_ns as RFC3339. protoJson and jsonl keep the raw machine values (addresses base64, state/enum numeric) for lossless downstream parsing.
Framing. The text formats (protoJson, jsonl, csv, tsv) terminate each flush with a newline; the binary formats (protobufList, msgpack) do not. The newline is the marshaller's responsibility, so the same format frames correctly on every sink — jsonl over tcp is newline-delimited for log shippers, while protobufList over Kafka stays a clean length-delimited stream.
pkg/xtcp/destinations_core.go defines the destination interface and a registry. Each backend lives in its own destinations_<scheme>.go file and calls RegisterDestination from an init() guarded by a //go:build dest_<scheme> tag. A destination is selected at runtime with -dest <scheme>:<address>; it must have been compiled in (see build flavors). Registering the same scheme twice panics, which catches duplicate build-tag mistakes early.
| Scheme | Example -dest |
Build tag | File |
|---|---|---|---|
kafka |
kafka:127.0.0.1:9092 |
dest_kafka |
destinations_kafka.go |
nats |
nats:nats:8222 |
dest_nats |
destinations_nats.go |
nsq |
nsq:nsqd:4150 |
dest_nsq |
destinations_nsq.go |
valkey |
valkey:valkey:6379 |
dest_valkey |
destinations_valkey.go |
s3parquet |
s3parquet:... |
dest_s3parquet |
destinations_s3parquet.go |
stdout |
stdout |
(always built) | destinations_stdout.go |
stderr |
stderr |
(always built) | destinations_file.go |
file |
file:/var/log/xtcp.jsonl |
(always built) | destinations_file.go |
tcp |
tcp:127.0.0.1:9000 |
(always built) | destinations_tcp.go |
http / https |
http://host:8080/ingest |
(always built) | destinations_http.go |
udp |
udp:127.0.0.1:13000 |
(always built) | destinations_udp.go |
unix |
unix:/tmp/xtcp.sock |
(always built) | destinations_unix.go |
unixgram |
unixgram:/tmp/xtcp.sock |
(always built) | destinations_unixgram.go |
null |
null |
(always built) | destinations_null.go |
The stdlib destinations (stdout, stderr, file, tcp, http/https, udp, unix, unixgram, null) are always compiled in; the library-backed ones (kafka, nats, nsq, valkey, s3parquet) are only present when their build tag is set. null discards output and is handy for benchmarking the collection path in isolation.
Notes on the stream sinks:
stdout/stderr/fileshare a smallio.Writer-backed core and write the marshalled bytes verbatim.stdoutis the easiest way to look at data — pair it with-marshal jsonl|csv|tsv; the daemon's logs go to stderr, so stdout carries only records (and in Docker they land indocker logs).fileappends (creating the file0600).tcpis the reliable, ordered transport most log/metric shippers (Vector, Logstash, Fluentd,nc) expect — xtcp2 has UDP too, but TCP is usually what you want for line-delimited text.http/httpsPOSTs each flushed batch to the URL; theContent-Typeis derived from the marshaller (application/x-ndjsonforjsonl,text/csv,text/tab-separated-values,application/json, elseapplication/octet-stream). Non-2xx responses are treated as errors. The POST timeout reuses-produceTimeout(default 10s).
pkg/xtcp/destinations_kafka.go uses franz-go to produce length-delimited protobufList batches to a topic. Notable behavior:
- Compression (
-kafkaCompression): empty/autonegotiates a preference list (zstd,lz4,snappy,none) with the broker; or pin one ofzstd,lz4,snappy,gzip,none. All are decodable by Redpanda and ClickHouse's Kafka engine. - Schema registry (
-kafkaSchemaUrl): thextcp_flat_recordproto can be registered with a Confluent-compatible schema registry. This is informational — ClickHouse's ProtobufList ingestion does not require it. The standaloneregister_schemabinary does the registration;-xtcpProtoFilepoints at the proto used. - Produce timeout (
-produceTimeout) bounds each produce call.
pkg/xtcp/destinations_s3parquet.go (build tag dest_s3parquet) writes Hive-partitioned Parquet files to an S3-compatible store (e.g. MinIO) instead of streaming to a broker. The record-to-Parquet schema mapping is in destinations_s3parquet_schema.go. Files are partitioned host=…/date=…/hour=…/<file>.parquet and finalized/uploaded when the in-memory builder crosses -s3ParquetFlushBytes (default 63 MiB uncompressed). Each row also carries a derived event_date column (per-row UTC date of timestamp_ns) so column-only readers (e.g. Snowflake Snowpipe) can prune/cluster without parsing the Hive path. Credentials and endpoint come from -s3* flags or S3_* environment variables; the bucket must already exist.
For a consumer-facing reference of the Parquet layout, schema, and the key TCP columns — written for a data team ingesting this into an enterprise platform — see parquet-format.md.
The per-socket record and its batch wrapper are defined in proto/xtcp_flat_record/v1/xtcp_flat_record.proto:
Envelope— a batch: repeatedXtcpFlatRecordrows plus metadata.XtcpFlatRecord— one socket snapshot: timestamp, hostname, network namespace, TCP state, thetcp_infofields, congestion algorithm, and the optional attribute groups (skmem, shutdown, DCTCP, BBR, sockopt, class/cgroup IDs). The free-form-label/-tagand the-locationvalue are embedded into every record; with-resolveContainerIdthe owning container'scontainer_id/container_runtime(resolved from the socket's cgroup) are too.
Generated Go types live in gen/go/xtcp_flat_record/. For the full schema reference (all three protobufs, generated bindings, and how to regenerate) see protobuf formats.
These exercise the analysis-friendly formats and sinks. xtcp2 needs CAP_NET_ADMIN + CAP_SYS_ADMIN (run as root / sudo), plus CAP_SYS_PTRACE for multi-namespace discovery (reading /proc/<pid>/ns/net; without it only the host namespace is seen — see network namespaces); use a low non-zero -d (e.g. -d 1) so logs stay quiet on stderr while records go to the chosen sink.
Local binary:
# JSON Lines to stdout, piped to jq
sudo ./result/bin/xtcp2 -dest stdout -marshal jsonl -d 1 | jq .
# CSV of just the columns you care about, into a file → open in DuckDB/R
sudo ./result/bin/xtcp2 -dest file:/tmp/socks.csv -marshal csv -d 1 \
-columns hostname,inetDiagMsgSocketSource,inetDiagMsgSocketSourcePort,inetDiagMsgState,congestionAlgorithmEnum,tcpInfoRtt
duckdb -c "select inetDiagMsgState, count(*) from '/tmp/socks.csv' group by 1"
# Stream NDJSON over TCP to a log shipper / nc
sudo ./result/bin/xtcp2 -dest tcp:127.0.0.1:9000 -marshal jsonl -d 1
# POST batches to an HTTP ingest endpoint
sudo ./result/bin/xtcp2 -dest http://127.0.0.1:8080/ingest -marshal jsonl -d 1In Docker (mount the host namespace dirs so the daemon sees real sockets; records on stdout/stderr appear in docker logs):
# one-time: expose the host root netns as a named netns
sudo mkdir -p /run/netns && sudo ip netns attach xtcp2host 1
# CSV to docker logs
docker run -d --name xtcp2 \
--cap-add NET_ADMIN --cap-add SYS_ADMIN \
-v /run/netns:/run/netns:ro -v /run/docker/netns:/run/docker/netns:ro \
xtcp2:min -dest stdout -marshal csv -d 1 -columns hostname,inetDiagMsgSocketSource,inetDiagMsgState
docker logs xtcp2 # header + humanized rowsCaveat for short-interval demos:
-frequency(poll interval, default10s) must stay greater than-timeout(per-namespace poll timeout, default5s) — config validation rejects e.g.-frequency 2sagainst the default-timeout 5s. To flush quickly for atcp/httpdemo use a valid pair like-frequency 3s -timeout 1s. The first batch only ships at the first flush (≈ one-frequencyinterval), so a receiver must stay open at least that long.
| Flag | Default | Purpose |
|---|---|---|
-marshal |
protobufList |
Output format: protobufList, protoJson, protoText, msgpack, jsonl, csv, tsv. |
-columns |
— | csv/tsv only: comma-separated subset of XtcpFlatRecord json field names; empty = all. |
-dest |
kafka:redpanda-0:9092 |
Destination scheme:address (see the destinations table). |
-topic |
xtcp |
Kafka / NSQ topic. |
-kafkaCompression |
auto |
Kafka compression codec or negotiation list. |
-kafkaSchemaUrl |
http://localhost:18081 |
Confluent schema registry URL. |
-xtcpProtoFile |
— | Proto file used when registering the schema. |
-produceTimeout |
— | Kafka produce timeout. |
-label, -tag |
— | Free-form strings embedded in every record (LABEL/TAG). |
-location |
— | Deployment grouping/facility (data center, PoP, region, …) stamped on every record's location (LOCATION). |
-hostname |
— | Hostname override stamped on records; defaults to os.Hostname(). Required in a container, where os.Hostname() returns the container id. XTCP_HOSTNAME (NOT HOSTNAME). |
-resolveContainerId |
false |
Resolve each socket's owning container id/runtime from its cgroup into container_id/container_runtime (CONTAINER_ID_RESOLVE). Needs /sys/fs/cgroup (mount it + --cgroupns=host in a container). |
-ipv4Ttl |
0 |
Outgoing IPv4 TTL on xtcp2's TCP listeners (Prometheus + gRPC); 0 = kernel default. A low value keeps replies from travelling far if the host is internet-exposed (IPV4_TTL). |
-ipv6HopLimit |
0 |
Outgoing IPv6 unicast hop limit on xtcp2's TCP listeners; 0 = kernel default (IPV6_HOP_LIMIT). |
-s3Endpoint |
— | S3 endpoint URL (or S3_ENDPOINT). |
-s3Bucket |
— | S3 bucket (or S3_BUCKET); must already exist. |
-s3Prefix |
— | Key prefix within the bucket. |
-s3AccessKey / -s3SecretKey |
— | S3 credentials (or S3_ACCESS_KEY / S3_SECRET_KEY); never logged. |
-s3Region |
us-east-1 |
S3 region. |
-s3SkipBucketProbe |
false |
Skip the startup BucketExists probe (a HeadBucket needing s3:ListBucket; or S3_SKIP_BUCKET_PROBE). Set for a write-only s3:PutObject-only credential. |
-s3ParquetFlushBytes |
0 → 63 MiB |
Parquet builder flush threshold (uncompressed). |
-destWriteFiles |
— | Also write marshalled output to N files (debugging). |
- Polling & batching — how Envelopes are formed before marshalling.
- Build flavors — selecting which destinations are compiled in.
- protobufList migration — the batch wire format in depth.
- Operations — the Kafka/Redpanda → ClickHouse pipeline.