Skip to content

Decode Tesla Fleet Telemetry protobuf for the NATS add-on for Splunk - #1

Merged
Bre77 merged 2 commits into
mainfrom
fm/telemetry-decoder-go
Oct 15, 2026
Merged

Bre77 merged 2 commits into
mainfrom
fm/telemetry-decoder-go

Conversation

@Bre77

@Bre77 Bre77 commented Oct 15, 2026

Copy link
Copy Markdown
Member

A Go payload decoder for the NATS Add-on for Splunk (Bre77/splunk_nats) that turns Tesla Fleet Telemetry protobuf on telemetry.<VIN>.{data,alerts,errors,connectivity,metrics} into one-line JSON events.

What it does

  • Implements the splunk_nats decoder protocol: one JSON request per line on stdin (subject, headers, base64 payload), one line out per request, flushed each time. The line is the event, or an empty line with a one-line reason on stderr. It exits 0 when stdin closes. Bad JSON, bad base64, an empty payload, a payload that is not valid protobuf, an unknown subject suffix and even a panic each get an empty line, and the decoder keeps serving.
  • Picks the message type from the subject's last token. In 2 minutes of live telemetry.> capture (12,989 messages, 790 vehicles), every subject had 3 tokens and a suffix of data, alerts, connectivity or errors. There was no metrics stream, but metrics is mapped to VehicleMetrics anyway.
  • Output (decision): JSON field names follow the protobuf JSON mapping, like fleet-telemetry's own transmit_decoded_records JSON: lowerCamel (createdAt, networkInterface), enums as names, every field emitted. By default, vehicle data is flattened to one top-level field per signal, so Splunk extracts VehicleSpeed, Location.latitude and DoorState.DriverFront directly. Invalid signals are listed in invalid. A signal or value type that is newer than the protos is kept (named by its number, or null), not dropped.
    • -format protojson gives fleet-telemetry's exact key/value shape.
    • -proto-names gives snake_case (created_at, network_interface). That matches the Python example decoder in the splunk_nats README, whose example searches (status network_interface) use proto names.
  • The VIN comes from the message, or from the subject when the message has none.

Generation and builds

  • internal/protos is generated by scripts/generate.sh and committed, with these pins:

    • Teslemetry/tesla-protocol e6f7afb1b633d7215e34d0cdac66dab983c6ae78
    • protoc 36.2, checksum-verified
    • protoc-gen-go v1.36.12, via the tool directive in go.mod

    CI regenerates the code and fails on any diff. A local rerun is byte-identical.

  • scripts/build.sh builds static CGO_ENABLED=0 binaries for linux/amd64, linux/arm64, darwin/amd64 and darwin/arm64, plus SHA256SUMS. Two builds of the same commit are byte-identical.

  • .github/workflows/ci.yml runs gofmt, go vet, go test -race, the benchmarks, the generated-code check and the build. release.yml runs on v* tags: it tests, builds and creates a GitHub release with the binaries and SHA256SUMS. No tag was created. The Go toolchain is pinned to go1.27.2, and the actions are pinned by SHA.

Tests

  • Golden tests run 10 real payloads from live traffic through all 4 output options:

    • data: full, location, invalid, doors, resend
    • alerts with an active alert
    • errors and errors-empty
    • connectivity connected and disconnected

    tools/mkfixtures anonymized them: fake VIN TESTVIN0000000001, fake connection IDs and UUIDs, and Fremont factory coordinates. Destination, vehicle and media names were replaced. None of the 790 live VINs appear anywhere in the repo, and that was checked.

  • Unit tests cover every value kind, duplicate signals, signals and enum values newer than the protos, float32 formatting, JSON string escaping and invalid UTF-8.

  • Protocol tests cover bad base64, an unknown subject, empty and missing payloads, non-protobuf, non-JSON, an empty line, CRLF, EOF with no input, a last line without a newline, a 1 MB request, and per-line flushing over a pipe.

  • A binary test builds the static binary and drives it as the add-on does: a request, waiting for the answer, then closing stdin and expecting exit 0.

  • Throughput:

    • BenchmarkRun (protocol loop, fixture mix): about 15 µs per message, about 65,000 msg/s.
    • Replaying 103,912 real captured messages through the binary takes 0.51 s, about 200,000 msg/s.
    • The live stream runs at about 110 msg/s.
  • golangci-lint v2 (latest) reports 0 issues, and actionlint is clean.

End-to-end proof

scripts/e2e-splunk.sh (in this PR) uses a disposable Splunk container:

  • splunk_nats is built from current main (9c5d52a) and installed.
  • The decoder goes in as local/decoders/telemetry-decoder, with an account on the local hq leaf nats://host.docker.internal:4222. Credentials come from 1Password over curl stdin and are never in argv or on disk.
  • It runs a natssubscribe ... decoder=telemetry-decoder search, then a NATS Subscribe input on telemetry.> for 5 minutes.
  • The run fails if any event falls back to nats:message (base64) or if splunkd.log has any "failed to decode".

Containers are removed with docker rm -fv. Output below has VINs and locations aggregated away.

Results with the final commit (67edffa), 5 minutes of live telemetry.> each:

Splunk 10.4.1 Splunk latest (10.6.0.5)
natssubscribe sample (2000 msgs) all decoded, nats:json, no decode_error all decoded, nats:json, no decode_error
indexed: data / alerts / connectivity / errors 29,075 / 343 / 347 / 30 29,373 / 348 / 346 / 30
base64 fallback events (nats:message) 0 0
"failed to decode" in splunkd.log 0 0
events without vin or createdAt extracted 0 0
vehicles (data) 885 883

Fields extracted directly from the decoded data events, 10.4.1 run:

events  VehicleSpeed  Location.latitude  BatteryLevel  Gear  DoorState.DriverFront  invalid{}
29075   1566          4442               2055          36    256                    707
avg_VehicleSpeed=26.28  avg_BatteryLevel=61.02  Gear values: ShiftStateD ShiftStateN ShiftStateP ShiftStateR
connectivity: status x networkInterface = CONNECTED/DISCONNECTED x cellular/wifi/""
alerts{}.name top: UI_a266_DashcamRecentBufferDurationReduced, VCFRONT_a361_washerFluidLowMomentary, APP_w269_autopilotLimited, UI_a002_DoorOpen
errors{}.name: unsuported_interval

The first run on the earlier commit also passed with 0 failures (29,242 and 29,583 events), but in its natssubscribe sample one large alert event had no vin extracted. protojson puts vin after the alerts array, and Splunk stops extracting JSON fields partway into big events. The second commit puts vin and createdAt first in every flat event, and the E2E now asserts it.

Notes

  • Nothing was installed on /opt/splunk, and no tags were created.
  • Live errors messages were unsupported_field (for example LifetimeEnergyGainedRegen) and unsuported_interval (Tesla's spelling). Vehicles send these about the fleet-telemetry config, so they are not a decoder issue.
  • Gear arrives both as the ShiftState enum (ShiftStateD) and, from some vehicles, as a plain string (P, R). One run saw both. The decoder keeps values as sent, as fleet-telemetry does, so a search across both forms has to match both.
  • The arm64 binary was checked only as a valid static aarch64 ELF. This host has no arm64 emulation, so it was not executed.

Bre77 added 2 commits October 15, 2026 10:30
A static Go binary implementing the splunk_nats decoder protocol: one JSON
request per line on stdin, one JSON event (or an empty line plus a stderr
reason) per line on stdout, flushed per line, exiting when stdin closes.

- The record type comes from the subject's last token (data, alerts, errors,
  connectivity, metrics), verified against live telemetry.> traffic.
- Vehicle data is flattened to one top-level field per signal by default
  ("VehicleSpeed": 28.6, "Location": {"latitude": ...}), with invalid signals
  listed in "invalid"; -format protojson gives fleet-telemetry's own JSON.
  Field names follow the protobuf JSON mapping (createdAt); -proto-names
  switches to created_at.
- Messages are generated from Teslemetry/tesla-protocol at a pinned commit by
  scripts/generate.sh with pinned protoc and protoc-gen-go; CI checks the
  committed code matches.
- Golden tests on anonymized live payloads for every record type, protocol and
  binary tests, and throughput benchmarks.
- CI (gofmt, vet, race tests, benchmarks, generated code, build) and a
  tag-triggered release with linux/darwin amd64/arm64 binaries and SHA256SUMS.
- scripts/e2e-splunk.sh proves it in a disposable Splunk with the add-on
  against live NATS.
Alerts and errors carry vin and createdAt after a long array (alert events
reach 16 KB), past where Splunk stops extracting fields at search time: a live
natssubscribe sample had an alert event without vin. The flat format now starts
every record type with vin and createdAt; -format protojson keeps proto order.
The Splunk E2E checks every indexed event has both.
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: Teslemetry/coderabbit/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Free
  • Run ID: fe336872-f917-45bc-87a2-4bdb4947b4bf
📥 Commits

Reviewing files that changed from the base of the PR and between 05a46a6 and 67edffa.

⛔ Files ignored due to path filters: 6
  • go.sum is excluded by !**/*.sum
  • internal/protos/vehicle_alert.pb.go is excluded by !**/*.pb.go
  • internal/protos/vehicle_connectivity.pb.go is excluded by !**/*.pb.go
  • internal/protos/vehicle_data.pb.go is excluded by !**/*.pb.go
  • internal/protos/vehicle_error.pb.go is excluded by !**/*.pb.go
  • internal/protos/vehicle_metric.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing: 68
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .gitignore
  • README.md
  • go.mod
  • internal/decoder/bench_test.go
  • internal/decoder/decoder.go
  • internal/decoder/decoder_test.go
  • internal/decoder/golden_test.go
  • internal/decoder/protocol.go
  • internal/decoder/protocol_test.go
  • internal/decoder/testdata/golden/alerts.flat.json
  • internal/decoder/testdata/golden/alerts.flat.proto-names.json
  • internal/decoder/testdata/golden/alerts.protojson.json
  • internal/decoder/testdata/golden/alerts.protojson.proto-names.json
  • internal/decoder/testdata/golden/alerts.request
  • internal/decoder/testdata/golden/connectivity-connected.flat.json
  • internal/decoder/testdata/golden/connectivity-connected.flat.proto-names.json
  • internal/decoder/testdata/golden/connectivity-connected.protojson.json
  • internal/decoder/testdata/golden/connectivity-connected.protojson.proto-names.json
  • internal/decoder/testdata/golden/connectivity-connected.request
  • internal/decoder/testdata/golden/connectivity-disconnected.flat.json
  • internal/decoder/testdata/golden/connectivity-disconnected.flat.proto-names.json
  • internal/decoder/testdata/golden/connectivity-disconnected.protojson.json
  • internal/decoder/testdata/golden/connectivity-disconnected.protojson.proto-names.json
  • internal/decoder/testdata/golden/connectivity-disconnected.request
  • internal/decoder/testdata/golden/data-doors.flat.json
  • internal/decoder/testdata/golden/data-doors.flat.proto-names.json
  • internal/decoder/testdata/golden/data-doors.protojson.json
  • internal/decoder/testdata/golden/data-doors.protojson.proto-names.json
  • internal/decoder/testdata/golden/data-doors.request
  • internal/decoder/testdata/golden/data-full.flat.json
  • internal/decoder/testdata/golden/data-full.flat.proto-names.json
  • internal/decoder/testdata/golden/data-full.protojson.json
  • internal/decoder/testdata/golden/data-full.protojson.proto-names.json
  • internal/decoder/testdata/golden/data-full.request
  • internal/decoder/testdata/golden/data-invalid.flat.json
  • internal/decoder/testdata/golden/data-invalid.flat.proto-names.json
  • internal/decoder/testdata/golden/data-invalid.protojson.json
  • internal/decoder/testdata/golden/data-invalid.protojson.proto-names.json
  • internal/decoder/testdata/golden/data-invalid.request
  • internal/decoder/testdata/golden/data-location.flat.json
  • internal/decoder/testdata/golden/data-location.flat.proto-names.json
  • internal/decoder/testdata/golden/data-location.protojson.json
  • internal/decoder/testdata/golden/data-location.protojson.proto-names.json
  • internal/decoder/testdata/golden/data-location.request
  • internal/decoder/testdata/golden/data-resend.flat.json
  • internal/decoder/testdata/golden/data-resend.flat.proto-names.json
  • internal/decoder/testdata/golden/data-resend.protojson.json
  • internal/decoder/testdata/golden/data-resend.protojson.proto-names.json
  • internal/decoder/testdata/golden/data-resend.request
  • internal/decoder/testdata/golden/errors-empty.flat.json
  • internal/decoder/testdata/golden/errors-empty.flat.proto-names.json
  • internal/decoder/testdata/golden/errors-empty.protojson.json
  • internal/decoder/testdata/golden/errors-empty.protojson.proto-names.json
  • internal/decoder/testdata/golden/errors-empty.request
  • internal/decoder/testdata/golden/errors.flat.json
  • internal/decoder/testdata/golden/errors.flat.proto-names.json
  • internal/decoder/testdata/golden/errors.protojson.json
  • internal/decoder/testdata/golden/errors.protojson.proto-names.json
  • internal/decoder/testdata/golden/errors.request
  • internal/protos/source.go
  • main.go
  • main_test.go
  • scripts/build.sh
  • scripts/e2e-splunk.sh
  • scripts/generate.sh
  • tools/mkfixtures/main.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

This pull request adds a Go telemetry decoder with flat and ProtoJSON output, a JSON-lines command-line protocol, fixtures and tests, and scripts and workflows for generation, builds, releases, and Splunk integration.

Changes: Telemetry decoder

Layer / File(s) Summary
Decoder serialization
internal/decoder/decoder.go, internal/decoder/decoder_test.go
Adds flat and ProtoJSON formats for data, alerts, errors, connectivity, and metrics records. Flat data output handles repeated and invalid signals. Tests cover serialization, record selection, errors, and JSON encoding.
Request protocol and CLI
internal/decoder/protocol.go, main.go, internal/decoder/protocol_test.go, main_test.go
Adds newline-delimited JSON requests with base64 payloads, per-request responses and diagnostics, and a CLI with format and version options. Tests cover protocol errors, long requests, stream behavior, and executable arguments.
Golden fixtures and validation
internal/decoder/golden_test.go, internal/decoder/testdata/golden/*, tools/mkfixtures/main.go, internal/decoder/bench_test.go
Adds golden request and output fixtures for telemetry record types, fixture selection and anonymization, golden comparisons, and decode and stream benchmarks.
Generation, build, and release
go.mod, internal/protos/source.go, scripts/generate.sh, scripts/build.sh, .github/workflows/*, .gitignore, README.md
Adds module and protocol-source metadata, protobuf generation and cross-platform build scripts, CI and tag-triggered release workflows, and project documentation.
Splunk end-to-end check
scripts/e2e-splunk.sh
Adds a disposable Splunk integration check that runs NATS decoding, inspects indexed events, and reports decode failures and missing fields.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant DecoderRun as Decoder.Run
  participant RequestPayload as Request.DecodePayload
  participant DecoderDecode as Decoder.Decode
  participant Output as stdout
  participant Diagnostics as stderr
  CLI->>DecoderRun: Run with stdin, stdout, and diagnostics
  loop Each input line
    DecoderRun->>RequestPayload: Decode standard-base64 payload
    RequestPayload-->>DecoderRun: Payload bytes or error
    DecoderRun->>DecoderDecode: Decode subject and payload
    DecoderDecode-->>DecoderRun: JSON event or error
    DecoderRun->>Output: Write and flush event line
    DecoderRun->>Diagnostics: Write handling error when present
  end
Loading
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@Bre77
Bre77 merged commit 182a31c into main Oct 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant