feat(obs): add telemetry stack
This commit is contained in:
parent
10c8e2c884
commit
a622739053
11 changed files with 395 additions and 0 deletions
23
observability/README.md
Normal file
23
observability/README.md
Normal 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.
|
||||
46
observability/alerts/nereus.yaml
Normal file
46
observability/alerts/nereus.yaml
Normal 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.
|
||||
57
observability/compose.yaml
Normal file
57
observability/compose.yaml
Normal 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:
|
||||
92
observability/grafana/dashboards/nereus-api.json
Normal file
92
observability/grafana/dashboards/nereus-api.json
Normal 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
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: Nereus
|
||||
folder: Nereus
|
||||
type: file
|
||||
disableDeletion: true
|
||||
editable: false
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
|
@ -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
|
||||
28
observability/loki/loki.yaml
Normal file
28
observability/loki/loki.yaml
Normal 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
|
||||
44
observability/otel-collector/config.yaml
Normal file
44
observability/otel-collector/config.yaml
Normal 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]
|
||||
25
observability/otel-collector/local.yaml
Normal file
25
observability/otel-collector/local.yaml
Normal 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]
|
||||
12
observability/prometheus/prometheus.yaml
Normal file
12
observability/prometheus/prometheus.yaml
Normal 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]
|
||||
17
observability/tempo/tempo.yaml
Normal file
17
observability/tempo/tempo.yaml
Normal 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
|
||||
Loading…
Add table
Reference in a new issue