feat(obs): add telemetry stack

This commit is contained in:
Alex 2026-08-24 23:17:53 +02:00
parent 10c8e2c884
commit a622739053
11 changed files with 395 additions and 0 deletions

23
observability/README.md Normal file
View file

@ -0,0 +1,23 @@
# Nereus observability
Start the application and its local observability overlay with one command:
```bash
docker compose -f compose.yaml -f observability/compose.yaml up -d
```
The local endpoints are:
- Grafana: `http://127.0.0.1:13000`
- Prometheus: `http://127.0.0.1:19090`
- API: `http://127.0.0.1:18080`
Grafana uses anonymous administrator access only in this local overlay. No
password or credential is stored in the repository. Stop the complete stack
with the same two Compose files and `down`.
`otel-collector/local.yaml` handles local API traces. `otel-collector/config.yaml`
is the cluster configuration: it receives traces and tails only pod logs under
the `nereus` namespace path, then exports traces to Tempo and logs to Loki. Log
collection and trace-to-log links therefore require the real Kubernetes
environment and are not simulated in local Compose.

View file

@ -0,0 +1,46 @@
groups:
- name: nereus-api
rules:
- alert: NereusAPIHighErrorRate
expr: |
sum(rate(nereus_http_requests_total{path=~"/api/v1/.*",status=~"5.."}[2m]))
/
clamp_min(sum(rate(nereus_http_requests_total{path=~"/api/v1/.*"}[2m])), 0.001)
> 0.05
for: 1m
labels:
severity: critical
annotations:
summary: Nereus API error rate exceeds 5 percent
description: The API has returned more than 5 percent HTTP 5xx responses for one minute.
- alert: NereusAPIHighLatency
expr: |
histogram_quantile(
0.95,
sum by (le) (rate(nereus_http_request_duration_seconds_bucket{path=~"/api/v1/.*"}[5m]))
) > 1
for: 2m
labels:
severity: warning
annotations:
summary: Nereus API p95 latency exceeds one second
description: The API p95 request duration has exceeded one second for two minutes.
- alert: NereusAPIReadinessFailing
expr: sum(rate(nereus_http_requests_total{path="/readyz",status="503"}[2m])) > 0
for: 1m
labels:
severity: critical
annotations:
summary: Nereus API cannot reach its database
description: Readiness requests have returned HTTP 503 for one minute.
- alert: NereusAPIDown
expr: up{job="nereus-api"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: Prometheus cannot scrape the Nereus API
description: The Nereus API metrics endpoint has been unreachable for one minute.

View file

@ -0,0 +1,57 @@
services:
prometheus:
image: prom/prometheus:v3.14.0
command:
- --config.file=/etc/prometheus/prometheus.yaml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=6h
ports:
- "${PROMETHEUS_PORT:-19090}:9090"
volumes:
- ./observability/prometheus/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro
- ./observability/alerts:/etc/prometheus/rules:ro
- prometheus-data:/prometheus
tempo:
image: grafana/tempo:3.0.3
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./observability/tempo/tempo.yaml:/etc/tempo.yaml:ro
tmpfs:
- /var/tempo:uid=10001,gid=10001,mode=0755
loki:
image: grafana/loki:3.7.6
command: ["-config.file=/etc/loki/local-config.yaml"]
volumes:
- ./observability/loki/loki.yaml:/etc/loki/local-config.yaml:ro
tmpfs:
- /var/loki:uid=10001,gid=10001,mode=0755
otel-collector:
volumes:
- ./observability/otel-collector/local.yaml:/etc/otelcol-contrib/config.yaml:ro
depends_on:
- tempo
- loki
grafana:
image: grafana/grafana:13.2.0
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
GF_AUTH_DISABLE_LOGIN_FORM: "true"
ports:
- "${GRAFANA_PORT:-13000}:3000"
volumes:
- ./observability/grafana/provisioning:/etc/grafana/provisioning:ro
- ./observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
depends_on:
- prometheus
- tempo
- loki
volumes:
prometheus-data:
grafana-data:

View file

@ -0,0 +1,92 @@
{
"annotations": {"list": []},
"editable": false,
"graphTooltip": 1,
"panels": [
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"fieldConfig": {"defaults": {"unit": "reqps"}},
"gridPos": {"h": 8, "w": 8, "x": 0, "y": 0},
"id": 1,
"targets": [{"expr": "sum by (version) (rate(nereus_http_requests_total{path=~\"/api/v1/.*\",version=~\"$version\"}[1m]))", "legendFormat": "{{version}}", "refId": "A"}],
"title": "API request rate",
"type": "timeseries"
},
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1}},
"gridPos": {"h": 8, "w": 8, "x": 8, "y": 0},
"id": 2,
"targets": [{"expr": "sum by (version) (rate(nereus_http_requests_total{path=~\"/api/v1/.*\",status=~\"5..\",version=~\"$version\"}[1m])) / clamp_min(sum by (version) (rate(nereus_http_requests_total{path=~\"/api/v1/.*\",version=~\"$version\"}[1m])), 0.001)", "legendFormat": "{{version}}", "refId": "A"}],
"title": "API error rate",
"type": "timeseries"
},
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"fieldConfig": {"defaults": {"unit": "s"}},
"gridPos": {"h": 8, "w": 8, "x": 16, "y": 0},
"id": 3,
"targets": [{"expr": "histogram_quantile(0.95, sum by (le, version) (rate(nereus_http_request_duration_seconds_bucket{path=~\"/api/v1/.*\",version=~\"$version\"}[5m])))", "legendFormat": "p95 {{version}}", "refId": "A"}],
"title": "API p95 latency",
"type": "timeseries"
},
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"fieldConfig": {"defaults": {"unit": "s"}},
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 8},
"id": 4,
"targets": [{"expr": "histogram_quantile(0.95, sum by (le, operation) (rate(nereus_db_query_duration_seconds_bucket[5m])))", "legendFormat": "{{operation}}", "refId": "A"}],
"title": "Database p95 latency",
"type": "timeseries"
},
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"fieldConfig": {"defaults": {"unit": "ops"}},
"gridPos": {"h": 8, "w": 6, "x": 12, "y": 8},
"id": 5,
"targets": [{"expr": "sum(rate(nereus_readings_ingested_total[1m]))", "refId": "A"}],
"title": "Readings ingested",
"type": "stat"
},
{
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"h": 8, "w": 6, "x": 18, "y": 8},
"id": 6,
"targets": [{"expr": "nereus_buoys_active", "refId": "A"}],
"title": "Active buoys",
"type": "stat"
},
{
"datasource": {"type": "loki", "uid": "loki"},
"gridPos": {"h": 9, "w": 24, "x": 0, "y": 16},
"id": 7,
"targets": [{"expr": "{k8s_namespace_name=\"nereus\"} | json", "refId": "A"}],
"title": "Correlated API logs",
"type": "logs"
}
],
"refresh": "5s",
"schemaVersion": 42,
"tags": ["nereus"],
"templating": {
"list": [
{
"allValue": ".*",
"current": {"text": "All", "value": "$__all"},
"datasource": {"type": "prometheus", "uid": "prometheus"},
"definition": "label_values(nereus_http_requests_total, version)",
"includeAll": true,
"label": "Version",
"name": "version",
"query": {"query": "label_values(nereus_http_requests_total, version)", "refId": "StandardVariableQuery"},
"refresh": 1,
"type": "query"
}
]
},
"time": {"from": "now-30m", "to": "now"},
"timezone": "browser",
"title": "Nereus API",
"uid": "nereus-api",
"version": 1
}

View file

@ -0,0 +1,10 @@
apiVersion: 1
providers:
- name: Nereus
folder: Nereus
type: file
disableDeletion: true
editable: false
options:
path: /var/lib/grafana/dashboards

View file

@ -0,0 +1,41 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
jsonData:
timeInterval: 5s
- name: Loki
uid: loki
type: loki
access: proxy
url: http://loki:3100
editable: false
jsonData:
derivedFields:
- name: TraceID
matcherRegex: '"trace_id":"([a-f0-9]{32})"'
datasourceUid: tempo
url: '${__value.raw}'
- name: Tempo
uid: tempo
type: tempo
access: proxy
url: http://tempo:3200
editable: false
jsonData:
tracesToLogsV2:
datasourceUid: loki
spanStartTimeShift: -1m
spanEndTimeShift: 1m
tags:
- key: service.name
value: service_name
filterByTraceID: true

View file

@ -0,0 +1,28 @@
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /var/loki
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
filesystem:
directory: /var/loki/chunks
analytics:
reporting_enabled: false

View file

@ -0,0 +1,44 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
file_log/nereus:
include:
- /var/log/pods/nereus_*/*/*.log
include_file_path: true
operators:
- type: container
id: parse-container-log
processors:
memory_limiter:
check_interval: 1s
limit_mib: 256
k8s_attributes:
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.container.name
batch:
timeout: 5s
exporters:
otlp_grpc/tempo:
endpoint: tempo:4317
tls:
insecure: true
otlp_http/loki:
endpoint: http://loki:3100/otlp
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8s_attributes, batch]
exporters: [otlp_grpc/tempo]
logs:
receivers: [file_log/nereus]
processors: [memory_limiter, k8s_attributes, batch]
exporters: [otlp_http/loki]

View file

@ -0,0 +1,25 @@
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
memory_limiter:
check_interval: 1s
limit_mib: 256
batch:
timeout: 5s
exporters:
otlp_grpc/tempo:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp_grpc/tempo]

View file

@ -0,0 +1,12 @@
global:
scrape_interval: 5s
evaluation_interval: 5s
rule_files:
- /etc/prometheus/rules/*.yaml
scrape_configs:
- job_name: nereus-api
metrics_path: /metrics
static_configs:
- targets: [api:8080]

View file

@ -0,0 +1,17 @@
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
storage:
trace:
backend: local
wal:
path: /var/tempo/wal
local:
path: /var/tempo/traces