Skip to content

Commit ef579af

Browse files
committed
Add Tempo tracing backend and fix dashboard queries
Adds a Tempo container (compose + Helm) alongside the OTel collector for TraceQL, service-graph, and span-metrics dashboards, wiring the metrics generator to remote-write into Prometheus. Updates Grafana dashboard datasource UIDs, span-metric attribute names (OTel semconv 1.x: http.response.status_code, messaging.destination.name, etc.), and Prometheus scrape targets/ports for workers. Rewrites many Grafana panel queries that were previously disabled or stubbed (ADR-002 placeholders) to pull from their now-available source tables (climate_snapshots, motor_snapshots, security_events, drive_telemetry, tesla_battery_passport_ledger, etc.) instead of signal_log placeholders. Adds vehicle/drive picker dropdowns backed by SQL variables in several dashboards, adds an automation-worker service to docker-compose.dev.yml, exposes /metrics on all workers via promhttp, and improves HTTP route attribution in the tracing middleware by tagging spans with chi's matched route after routing completes. Also fixes the validate_grafana_signal_log.py script to use a relative dashboard path and substitute more template variables.
1 parent 7b43737 commit ef579af

92 files changed

Lines changed: 1036 additions & 809 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ GRAFANA_PORT=3001
3636
GRAFANA_USER=admin
3737
GRAFANA_PASSWORD=changeme
3838

39+
# Distributed tracing (optional — pair with: docker compose --profile tracing up)
40+
# Required for the Tempo, service-graph, span-metrics, and SLO trace dashboards.
41+
OTEL_ENABLED=false
42+
OTEL_ENDPOINT=otel-collector:4317
43+
OTEL_INSECURE=true
44+
3945
# MQTT
4046
MQTT_PORT=1883
4147

cmd/automation-worker/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
pahomqtt "github.com/eclipse/paho.mqtt.golang"
14+
"github.com/prometheus/client_golang/prometheus/promhttp"
1415
"github.com/rs/zerolog"
1516
"github.com/rs/zerolog/log"
1617

@@ -295,6 +296,7 @@ func main() {
295296
port := healthPort()
296297
healthMux := http.NewServeMux()
297298
healthMux.HandleFunc("/healthz", healthHandler(db))
299+
healthMux.Handle("/metrics", promhttp.Handler())
298300
go func() {
299301
log.Info().Str("port", port).Msg("health endpoint listening")
300302
if err := http.ListenAndServe(":"+port, healthMux); err != nil && err != http.ErrServerClosed {

cmd/export-worker/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
backupmodel "github.com/ev-dev-labs/teslasync/internal/models/backup"
1414

15+
"github.com/prometheus/client_golang/prometheus/promhttp"
1516
"github.com/rs/zerolog"
1617
"github.com/rs/zerolog/log"
1718
"go.opentelemetry.io/otel"
@@ -211,6 +212,7 @@ func main() {
211212
healthPort := resolveHealthPort()
212213
healthMux := http.NewServeMux()
213214
healthMux.HandleFunc("/healthz", newHealthHandler(db))
215+
healthMux.Handle("/metrics", promhttp.Handler())
214216
go func() {
215217
log.Info().Str("port", healthPort).Msg("health endpoint listening")
216218
if err := http.ListenAndServe(":"+healthPort, healthMux); err != nil && err != http.ErrServerClosed {

cmd/notification-worker/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
alertmodel "github.com/ev-dev-labs/teslasync/internal/models/alert"
1717

18+
"github.com/prometheus/client_golang/prometheus/promhttp"
1819
"github.com/rs/zerolog"
1920
"github.com/rs/zerolog/log"
2021
"go.opentelemetry.io/otel"
@@ -384,6 +385,7 @@ func main() {
384385
}
385386
healthMux := http.NewServeMux()
386387
healthMux.HandleFunc("/healthz", healthzHandler(db))
388+
healthMux.Handle("/metrics", promhttp.Handler())
387389
go func() {
388390
log.Info().Str("port", healthPort).Msg("health endpoint listening")
389391
if err := http.ListenAndServe(":"+healthPort, healthMux); err != nil && err != http.ErrServerClosed {

cmd/slogen/generate_dashboards.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func runGenerateDashboards(args []string) error {
145145
return nil
146146
}
147147

148-
var prometheusDS = datasource{Type: "prometheus", UID: "prometheus"}
148+
var prometheusDS = datasource{Type: "prometheus", UID: "DS_TESLASYNC_PROMETHEUS"}
149149

150150
func renderSLODashboard(s SLO) (string, error) {
151151
var min0 float64 = 0

docker-compose.dev.yml

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# docker-compose.dev.yml — Development stack with ALL components enabled.
22
# Usage: docker compose -f docker-compose.dev.yml up --build
33
#
4-
# Includes: API, Web (Nginx), Notification Worker, Export Worker,
5-
# PostgreSQL, Redis, Mosquitto, Grafana, Jaeger (tracing).
4+
# Includes: API, Web (Nginx), Notification Worker, Export Worker, Automation Worker,
5+
# PostgreSQL, Redis, Mosquitto, Grafana, Tempo + Jaeger (tracing).
66
# All tracing and telemetry features are enabled by default.
77
#
88
# ⚠️ TimescaleDB-HA migration (ADR-007):
@@ -51,7 +51,7 @@ services:
5151
- FLEET_TELEMETRY_FALLBACK_POLL_INTERVAL=5m
5252
# Tracing enabled by default in dev
5353
- OTEL_ENABLED=true
54-
- OTEL_ENDPOINT=jaeger:4317
54+
- OTEL_ENDPOINT=otel-collector:4317
5555
- OTEL_SERVICE_NAME=teslasync-api
5656
- OTEL_INSECURE=true
5757
- GOOGLE_MAPS_API_KEY=${GOOGLE_MAPS_API_KEY:-}
@@ -63,7 +63,7 @@ services:
6363
condition: service_started
6464
redis:
6565
condition: service_healthy
66-
jaeger:
66+
otel-collector:
6767
condition: service_started
6868
healthcheck:
6969
test: ["CMD", "/usr/local/bin/teslasync", "healthcheck"]
@@ -104,14 +104,16 @@ services:
104104
- LOG_LEVEL=error
105105
- HEALTH_PORT=8081
106106
- OTEL_ENABLED=true
107-
- OTEL_ENDPOINT=jaeger:4317
107+
- OTEL_ENDPOINT=otel-collector:4317
108108
- OTEL_SERVICE_NAME=teslasync-notification-worker
109109
- OTEL_INSECURE=true
110110
depends_on:
111111
postgres:
112112
condition: service_healthy
113113
mosquitto:
114114
condition: service_started
115+
otel-collector:
116+
condition: service_started
115117
healthcheck:
116118
test: ["CMD", "/usr/local/bin/notification-worker", "healthcheck"]
117119
interval: 15s
@@ -139,21 +141,63 @@ services:
139141
- LOG_LEVEL=error
140142
- HEALTH_PORT=8082
141143
- OTEL_ENABLED=true
142-
- OTEL_ENDPOINT=jaeger:4317
144+
- OTEL_ENDPOINT=otel-collector:4317
143145
- OTEL_SERVICE_NAME=teslasync-export-worker
144146
- OTEL_INSECURE=true
145147
depends_on:
146148
postgres:
147149
condition: service_healthy
148150
mosquitto:
149151
condition: service_started
152+
otel-collector:
153+
condition: service_started
150154
healthcheck:
151155
test: ["CMD", "/usr/local/bin/export-worker", "healthcheck"]
152156
interval: 15s
153157
timeout: 5s
154158
retries: 3
155159
start_period: 10s
156160

161+
automation-worker:
162+
build:
163+
context: .
164+
dockerfile: Dockerfile.automation
165+
args:
166+
VERSION: dev
167+
container_name: teslasync-automation-worker-dev
168+
restart: unless-stopped
169+
environment:
170+
- DATABASE_HOST=postgres
171+
- DATABASE_PORT=5432
172+
- DATABASE_USER=teslasync
173+
- DATABASE_PASSWORD=teslasync
174+
- DATABASE_NAME=teslasync
175+
- DATABASE_SSL_MODE=disable
176+
- MQTT_HOST=mosquitto
177+
- MQTT_PORT=1883
178+
- TESLA_CLIENT_ID=${TESLA_CLIENT_ID:-dev-client-id}
179+
- TESLA_CLIENT_SECRET=${TESLA_CLIENT_SECRET:-dev-client-secret}
180+
- TESLA_API_BASE_URL=${TESLA_API_BASE_URL:-https://fleet-api.prd.na.vn.cloud.tesla.com}
181+
- LOG_LEVEL=error
182+
- HEALTH_PORT=8083
183+
- OTEL_ENABLED=true
184+
- OTEL_ENDPOINT=otel-collector:4317
185+
- OTEL_SERVICE_NAME=teslasync-automation-worker
186+
- OTEL_INSECURE=true
187+
depends_on:
188+
postgres:
189+
condition: service_healthy
190+
mosquitto:
191+
condition: service_started
192+
otel-collector:
193+
condition: service_started
194+
healthcheck:
195+
test: ["CMD", "/usr/local/bin/automation-worker", "healthcheck"]
196+
interval: 15s
197+
timeout: 5s
198+
retries: 3
199+
start_period: 10s
200+
157201
# ── Infrastructure ────────────────────────────────────────
158202

159203
postgres:
@@ -229,13 +273,35 @@ services:
229273
- POSTGRES_USER=teslasync
230274
- POSTGRES_PASSWORD=teslasync
231275
- POSTGRES_DB=teslasync
276+
- PROMETHEUS_URL=http://prometheus:9090
232277
volumes:
233278
- dev_grafana_data:/var/lib/grafana
234-
- ./grafana/provisioning:/etc/grafana/provisioning
279+
- ./grafana/custom.ini:/etc/grafana/grafana.ini:ro
280+
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
281+
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
235282
- ./grafana/dashboards:/var/lib/grafana/dashboards
236283
depends_on:
237284
postgres:
238285
condition: service_healthy
286+
prometheus:
287+
condition: service_started
288+
289+
prometheus:
290+
image: prom/prometheus:v2.51.0
291+
container_name: teslasync-prometheus-dev
292+
restart: unless-stopped
293+
ports:
294+
- "9099:9090"
295+
volumes:
296+
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
297+
- ./helm/teslasync/files/prometheus/recording-rules.yaml:/etc/prometheus/rules/recording-rules.yaml:ro
298+
- ./helm/teslasync/files/prometheus/alerting-rules.yaml:/etc/prometheus/rules/alerting-rules.yaml:ro
299+
- dev_prometheus_data:/prometheus
300+
command:
301+
- '--config.file=/etc/prometheus/prometheus.yml'
302+
- '--storage.tsdb.retention.time=15d'
303+
- '--web.enable-remote-write-receiver'
304+
- '--enable-feature=exemplar-storage'
239305

240306
# ── Observability ─────────────────────────────────────────
241307

@@ -245,13 +311,46 @@ services:
245311
restart: unless-stopped
246312
ports:
247313
- "16686:16686" # Jaeger UI
248-
- "4317:4317" # OTLP gRPC
249-
- "4318:4318" # OTLP HTTP
250314
environment:
251315
- COLLECTOR_OTLP_ENABLED=true
252316

317+
tempo:
318+
image: grafana/tempo:2.7.0
319+
container_name: teslasync-tempo-dev
320+
restart: unless-stopped
321+
command:
322+
- "-config.file=/etc/tempo/config.yaml"
323+
ports:
324+
- "3200:3200"
325+
volumes:
326+
- ./observability/tempo/config.yaml:/etc/tempo/config.yaml:ro
327+
- dev_tempo_data:/var/tempo
328+
depends_on:
329+
prometheus:
330+
condition: service_started
331+
332+
otel-collector:
333+
image: otel/opentelemetry-collector-contrib:0.103.1
334+
container_name: teslasync-otel-collector-dev
335+
restart: unless-stopped
336+
command:
337+
- "--config=/etc/otelcol/config.yaml"
338+
volumes:
339+
- ./observability/otel-collector/config.yaml:/etc/otelcol/config.yaml:ro
340+
ports:
341+
- "4317:4317"
342+
- "4318:4318"
343+
- "9464:9464"
344+
depends_on:
345+
jaeger:
346+
condition: service_started
347+
tempo:
348+
condition: service_started
349+
253350
volumes:
254351
dev_postgres_data:
255352
dev_redis_data:
256353
dev_mosquitto_data:
257354
dev_grafana_data:
355+
dev_prometheus_data:
356+
dev_tempo_data:

docker-compose.yml

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,9 @@ services:
295295
- PROMETHEUS_URL=http://prometheus:9090
296296
volumes:
297297
- grafana_data:/var/lib/grafana
298-
- ./grafana/provisioning:/etc/grafana/provisioning
298+
- ./grafana/custom.ini:/etc/grafana/grafana.ini:ro
299+
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
300+
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
299301
- ./grafana/dashboards:/var/lib/grafana/dashboards
300302
depends_on:
301303
postgres:
@@ -329,6 +331,7 @@ services:
329331
- '--config.file=/etc/prometheus/prometheus.yml'
330332
- '--storage.tsdb.retention.time=15d'
331333
- '--web.enable-lifecycle'
334+
- '--web.enable-remote-write-receiver'
332335
# exemplar-storage retains the trace_id/span_id labels attached to
333336
# histogram observations by internal/api/middleware.go +
334337
# internal/metrics/exemplar.go so Grafana can render the "View trace"
@@ -503,6 +506,33 @@ services:
503506
limits:
504507
memory: 256M
505508

509+
# Tempo (optional — docker compose --profile tracing up)
510+
# Grafana's TraceQL, service-map, and span-metrics dashboards use this
511+
# backend. Its metrics generator remote-writes RED and service-graph series
512+
# to the Prometheus service above.
513+
tempo:
514+
image: grafana/tempo:2.7.0
515+
container_name: teslasync-tempo
516+
restart: unless-stopped
517+
profiles:
518+
- tracing
519+
command:
520+
- "-config.file=/etc/tempo/config.yaml"
521+
ports:
522+
- "${TEMPO_PORT:-3200}:3200"
523+
volumes:
524+
- ./observability/tempo/config.yaml:/etc/tempo/config.yaml:ro
525+
- tempo_data:/var/tempo
526+
depends_on:
527+
prometheus:
528+
condition: service_started
529+
deploy:
530+
resources:
531+
limits:
532+
memory: 512M
533+
reservations:
534+
memory: 128M
535+
506536
# OpenTelemetry Collector (optional — docker compose --profile tracing up)
507537
# Dev variant of helm/teslasync/files/otel-collector/config.yaml — 100%
508538
# sampling (no tail policy), CORS-enabled OTLP HTTP receiver for browser
@@ -532,6 +562,8 @@ services:
532562
depends_on:
533563
jaeger:
534564
condition: service_started
565+
tempo:
566+
condition: service_started
535567
deploy:
536568
resources:
537569
limits:
@@ -697,5 +729,6 @@ volumes:
697729
mosquitto_data:
698730
redis_data:
699731
prometheus_data:
732+
tempo_data:
700733
pyroscope_data:
701-
vehicle_photos:
734+
vehicle_photos:

docs/deployment/docker.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ The rest of this page is the longer story — what each service does, when to en
2727

2828
## What's in `docker-compose.yml`
2929

30-
The Compose file defines **13 services** across five concerns. By default, 10 come up; 3 are opt-in via profiles plus Ollama in the `ai` profile.
30+
The Compose file defines the core application and data services plus optional profile-gated sidecars.
3131

3232
### Always-on (10 services)
3333

@@ -44,7 +44,7 @@ The Compose file defines **13 services** across five concerns. By default, 10 co
4444
| `grafana` | Provisioned dashboards (admin/admin default) | 3001 |
4545
| `prometheus` | Metrics scrape + retention | 9099 |
4646

47-
### Profile-gated (3 services)
47+
### Profile-gated services
4848

4949
Profiles let you opt into heavier sidecars only when you need them. Bring a profile up alongside the default stack:
5050

@@ -54,13 +54,24 @@ docker compose --profile <name> up -d
5454

5555
| Profile | Service | What it adds |
5656
| ----------- | ------------------------ | ---------------------------------------------------------------- |
57-
| `tracing` | `jaeger` | OpenTelemetry trace collector + UI on `:16686` |
57+
| `tracing` | `otel-collector`, `tempo`, `jaeger` | OTLP ingest, TraceQL storage/span metrics, and Jaeger UI on `:16686` |
5858
| `telemetry` | `fleet-telemetry` | Tesla Fleet Telemetry server on `:4443`; HTTPS endpoint required |
5959
| `commands` | `vehicle-command-proxy` | Signs commands for vehicles that require it; on `:4443` |
6060
| `ai` | `ollama` | Local LLM inference for Helix AI on `:11434` |
6161

6262
You can stack profiles: `docker compose --profile telemetry --profile commands --profile ai up -d`.
6363

64+
The tracing dashboards also require the application processes to emit spans. Set
65+
`OTEL_ENABLED=true` in `.env` before starting the profile, then run:
66+
67+
```bash
68+
docker compose --profile tracing up -d --build
69+
```
70+
71+
Changing `OTEL_ENABLED` causes Compose to recreate the API and workers with
72+
tracing enabled. Starting the profile without that setting only starts the
73+
backends; it cannot create application traces or span-derived metrics.
74+
6475
## When to use which profile
6576

6677
| Situation | Profile to add |

grafana/custom.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ enabled = false
33

44
[auth.anonymous]
55
enabled = true
6-
org_name = TeslaSync
6+
org_name = Main Org.
77
org_role = Viewer
88

99
[dashboards]

0 commit comments

Comments
 (0)