observability
What it demonstrates
basic with telemetry on, and a place to look at it:
- puddle exports every trace (
OTEL_TRACES_SAMPLER=always_on) over OTLP to an OpenTelemetry collector, which passes them on to Jaeger (in memory); - Prometheus scrapes
/metricson puddle’s management listener; - Grafana has Prometheus as a data source and a starter
puddledashboard: requests, latency and errors by operation, commits by outcome, conflicts and uncertain commits, metastore latency, authorization decisions, Go memory.
Grafana’s Jaeger data source uses a query API that Jaeger 2.21 removed, so traces are in Jaeger’s own UI rather than in Grafana.
Run it
cd examples/compose/observability
docker compose up| Catalog REST | http://localhost:8089/api/catalog, warehouse=default |
puddle’s /metrics | http://localhost:9090/metrics |
| Jaeger | http://localhost:16686 |
| Prometheus | http://localhost:9091 |
| Grafana | http://localhost:3000 (anonymous admin: demo only) |
Make a commit:
B=localhost:8089/api/catalog/v1/default
curl -X POST $B/namespaces -H 'Content-Type: application/json' \
-d '{"namespace":["demo"]}'
curl -X POST $B/namespaces/demo/tables -H 'Content-Type: application/json' \
-d '{"name":"t","schema":{"type":"struct","fields":[{"id":1,"name":"id","type":"long","required":false}]}}'
curl -X POST $B/namespaces/demo/tables/t -H 'Content-Type: application/json' \
-d '{"updates":[{"action":"set-properties","updates":{"owner":"me"}}]}'In Jaeger, search service puddle, operation POST updateTable: the
trace shows the commit’s steps (see Traces). In Prometheus or Grafana,
sum by (kind, outcome) (puddle_commit_duration_seconds_count) counts
it.
docker-compose.yml
docker-compose.yml
# Self-contained docker-compose for puddle with OpenTelemetry on:
# traces over OTLP to a collector and on to Jaeger, metrics scraped by
# Prometheus from puddle's management listener, and Grafana with
# Prometheus as its data source and a starter dashboard. (Grafana's
# Jaeger data source speaks the query API Jaeger 2.21 dropped, so
# traces are in Jaeger's own UI.) SQLite metastore,
# local-filesystem warehouse, no auth.
#
# Run:
# docker compose up
# Catalog REST: http://localhost:8089/api/catalog (warehouse=default)
# Metrics: http://localhost:9090/metrics (puddle's management listener)
# Jaeger UI: http://localhost:16686
# Prometheus: http://localhost:9091
# Grafana: http://localhost:3000 (dashboard "puddle"; anonymous admin)
#
# Make a commit, then find its trace in Jaeger (service puddle,
# operation "POST updateTable"):
# curl -X POST localhost:8089/api/catalog/v1/default/namespaces \
# -H 'Content-Type: application/json' -d '{"namespace":["demo"]}'
# curl -X POST localhost:8089/api/catalog/v1/default/namespaces/demo/tables \
# -H 'Content-Type: application/json' \
# -d '{"name":"t","schema":{"type":"struct","fields":[{"id":1,"name":"id","type":"long","required":false}]}}'
# curl -X POST localhost:8089/api/catalog/v1/default/namespaces/demo/tables/t \
# -H 'Content-Type: application/json' \
# -d '{"updates":[{"action":"set-properties","updates":{"owner":"me"}}]}'
#
# Every request is traced here (OTEL_TRACES_SAMPLER=always_on); see the
# observability docs for sampling in production.
configs:
puddle-config:
content: |
logging:
level: info
format: json
metastore:
type: sqlite
sqlite:
path: /var/lib/puddle/puddle.db
warehouses:
default:
location: file:///var/lib/puddle/warehouse
# The management listener: /metrics, /healthz, /readyz. The
# exporter, sampler and resource come from OTEL_* below.
telemetry:
enabled: true
addr: ":9090"
collector-config:
content: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
otlp_grpc/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp_grpc/jaeger]
prometheus-config:
content: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: puddle
static_configs:
- targets: ["puddle:9090"]
grafana-datasources:
content: |
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
grafana-dashboards:
content: |
apiVersion: 1
providers:
- name: puddle
type: file
options:
path: /var/lib/grafana/dashboards
grafana-dashboard-puddle:
content: |
{
"uid": "puddle",
"title": "puddle",
"schemaVersion": 39,
"time": {"from": "now-1h", "to": "now"},
"refresh": "30s",
"panels": [
{"type": "row", "title": "Requests", "gridPos": {"x": 0, "y": 0, "w": 24, "h": 1}},
{"type": "timeseries", "title": "Requests/s by operation",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 0, "y": 1, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "reqps"}},
"targets": [{"refId": "A", "legendFormat": "{{http_route}}",
"expr": "sum by (http_route) (rate(http_server_request_duration_seconds_count[$$__rate_interval]))"}]},
{"type": "timeseries", "title": "p95 latency by operation",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 8, "y": 1, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "s"}},
"targets": [{"refId": "A", "legendFormat": "{{http_route}}",
"expr": "histogram_quantile(0.95, sum by (le, http_route) (rate(http_server_request_duration_seconds_bucket[$$__rate_interval])))"}]},
{"type": "timeseries", "title": "Errors/s by type",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 16, "y": 1, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "reqps"}},
"targets": [{"refId": "A", "legendFormat": "{{http_response_status_code}} {{error_type}}",
"expr": "sum by (http_response_status_code, error_type) (rate(http_server_request_duration_seconds_count{error_type!=\"\"}[$$__rate_interval]))"}]},
{"type": "row", "title": "Commits", "gridPos": {"x": 0, "y": 9, "w": 24, "h": 1}},
{"type": "timeseries", "title": "Commits/s by outcome",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 0, "y": 10, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "ops"}},
"targets": [{"refId": "A", "legendFormat": "{{kind}} {{outcome}}",
"expr": "sum by (kind, outcome) (rate(puddle_commit_duration_seconds_count[$$__rate_interval]))"}]},
{"type": "timeseries", "title": "p95 commit duration",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 8, "y": 10, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "s"}},
"targets": [{"refId": "A", "legendFormat": "{{kind}}",
"expr": "histogram_quantile(0.95, sum by (le, kind) (rate(puddle_commit_duration_seconds_bucket{outcome=\"committed\"}[$$__rate_interval])))"}]},
{"type": "timeseries", "title": "Conflicts, requirement failures, uncertain commits",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 16, "y": 10, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "short"}},
"targets": [
{"refId": "A", "legendFormat": "conflicts {{kind}}",
"expr": "sum by (kind) (increase(puddle_commit_conflicts_total[$$__rate_interval]))"},
{"refId": "B", "legendFormat": "requirement failures {{kind}}",
"expr": "sum by (kind) (increase(puddle_commit_requirement_failures_total[$$__rate_interval]))"},
{"refId": "C", "legendFormat": "uncertain {{resolution}}",
"expr": "sum by (resolution) (increase(puddle_commit_uncertain_total[$$__rate_interval]))"}]},
{"type": "row", "title": "Dependencies", "gridPos": {"x": 0, "y": 18, "w": 24, "h": 1}},
{"type": "timeseries", "title": "p95 metastore operation",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 0, "y": 19, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "s"}},
"targets": [{"refId": "A", "legendFormat": "{{db_operation_name}}",
"expr": "histogram_quantile(0.95, sum by (le, db_operation_name) (rate(db_client_operation_duration_seconds_bucket[$$__rate_interval])))"}]},
{"type": "timeseries", "title": "Authorization decisions/s",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 8, "y": 19, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "ops"}},
"targets": [{"refId": "A", "legendFormat": "{{decision}}",
"expr": "sum by (decision) (rate(puddle_authz_decisions_total{probe=\"false\"}[$$__rate_interval]))"}]},
{"type": "timeseries", "title": "Go memory and goroutines",
"datasource": {"type": "prometheus", "uid": "prometheus"},
"gridPos": {"x": 16, "y": 19, "w": 8, "h": 8},
"fieldConfig": {"defaults": {"unit": "bytes"},
"overrides": [{"matcher": {"id": "byName", "options": "goroutines"},
"properties": [{"id": "unit", "value": "short"}, {"id": "custom.axisPlacement", "value": "right"}]}]},
"targets": [
{"refId": "A", "legendFormat": "memory used", "expr": "sum(go_memory_used_bytes)"},
{"refId": "B", "legendFormat": "goroutines", "expr": "go_goroutine_count"}]}
]
}
services:
puddle:
image: ghcr.io/ragnard/puddle:latest
ports: ["8089:8089", "9090:9090"]
environment:
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_TRACES_SAMPLER: always_on
OTEL_RESOURCE_ATTRIBUTES: deployment.environment.name=example
volumes:
- puddle-data:/var/lib/puddle
configs:
- source: puddle-config
target: /etc/puddle/puddle.yaml
command: ["-c", "/etc/puddle/puddle.yaml"]
restart: unless-stopped
otel-collector:
image: otel/opentelemetry-collector:0.161.0@sha256:b6d2b9a85b1029d05b5ad913150c1f014eed4ae99be81a1813ca5ade4a191913
configs:
- source: collector-config
target: /etc/otelcol/config.yaml
command: ["--config=/etc/otelcol/config.yaml"]
restart: unless-stopped
# All-in-one Jaeger v2: OTLP in, in-memory storage, the UI.
jaeger:
image: jaegertracing/jaeger:2.21.0@sha256:3d0ac795ff98aa04d1be04311d2dac6c25b4bfc8322dc02e53bc5b170c5018c3
ports: ["16686:16686"]
restart: unless-stopped
prometheus:
image: prom/prometheus:v3.15.0@sha256:efd719c99d83b060d9daefdcf00360461adf279f45ef5391f8d111892118753e
ports: ["9091:9090"]
configs:
- source: prometheus-config
target: /etc/prometheus/prometheus.yml
volumes:
- prometheus-data:/prometheus
restart: unless-stopped
grafana:
image: grafana/grafana:13.2.2@sha256:ac461fb352abc50da10a51c7d02462e9c05488f11f53f14b3ad79a8145f638a0
ports: ["3000:3000"]
environment:
# Demo only: anyone reaching port 3000 is an admin.
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
GF_AUTH_DISABLE_LOGIN_FORM: "true"
configs:
- source: grafana-datasources
target: /etc/grafana/provisioning/datasources/puddle.yaml
- source: grafana-dashboards
target: /etc/grafana/provisioning/dashboards/puddle.yaml
- source: grafana-dashboard-puddle
target: /var/lib/grafana/dashboards/puddle.json
restart: unless-stopped
volumes:
puddle-data:
prometheus-data: