summaryrefslogtreecommitdiff
path: root/gemfeed
diff options
context:
space:
mode:
Diffstat (limited to 'gemfeed')
-rw-r--r--gemfeed/2025-12-07-f3s-kubernetes-with-freebsd-part-8.gmi1086
-rw-r--r--gemfeed/2025-12-07-f3s-kubernetes-with-freebsd-part-8.gmi.tpl1050
-rw-r--r--gemfeed/atom.xml1435
3 files changed, 3418 insertions, 153 deletions
diff --git a/gemfeed/2025-12-07-f3s-kubernetes-with-freebsd-part-8.gmi b/gemfeed/2025-12-07-f3s-kubernetes-with-freebsd-part-8.gmi
index 44e6c79a..221f80cd 100644
--- a/gemfeed/2025-12-07-f3s-kubernetes-with-freebsd-part-8.gmi
+++ b/gemfeed/2025-12-07-f3s-kubernetes-with-freebsd-part-8.gmi
@@ -1,6 +1,6 @@
# f3s: Kubernetes with FreeBSD - Part 8: Observability
-> Published at 2025-12-06T23:58:24+02:00
+> Published at 2025-12-06T23:58:24+02:00, last updated Mon 09 Mar 09:33:08 EET 2026
This is the 8th blog post about the f3s series for my self-hosting demands in a home lab. f3s? The "f" stands for FreeBSD, and the "3s" stands for k3s, the Kubernetes distribution I use on FreeBSD-based physical machines.
@@ -45,6 +45,42 @@ This is the 8th blog post about the f3s series for my self-hosting demands in a
* ⇢ ⇢ ⇢ Installing Node Exporter on OpenBSD
* ⇢ ⇢ ⇢ Adding OpenBSD hosts to Prometheus
* ⇢ ⇢ ⇢ OpenBSD memory metrics compatibility
+* ⇢ ⇢ Enabling etcd metrics in k3s
+* ⇢ ⇢ ⇢ Configuring Prometheus to scrape etcd
+* ⇢ ⇢ ⇢ Verifying etcd metrics
+* ⇢ ⇢ ⇢ Complete persistence-values.yaml
+* ⇢ ⇢ ZFS Monitoring for FreeBSD Servers
+* ⇢ ⇢ ⇢ Node Exporter ZFS Collector
+* ⇢ ⇢ ⇢ Verifying ZFS Metrics
+* ⇢ ⇢ ⇢ ZFS Recording Rules
+* ⇢ ⇢ ⇢ Grafana Dashboards
+* ⇢ ⇢ ⇢ Deployment
+* ⇢ ⇢ ⇢ Verifying ZFS Metrics in Prometheus
+* ⇢ ⇢ ⇢ Key Metrics to Monitor
+* ⇢ ⇢ ⇢ ZFS Pool and Dataset Metrics via Textfile Collector
+* ⇢ ⇢ Distributed Tracing with Grafana Tempo
+* ⇢ ⇢ ⇢ Why Distributed Tracing?
+* ⇢ ⇢ ⇢ Deploying Grafana Tempo
+* ⇢ ⇢ ⇢# Configuration Strategy
+* ⇢ ⇢ ⇢# Tempo Deployment Files
+* ⇢ ⇢ ⇢# Installation
+* ⇢ ⇢ ⇢ Configuring Grafana Alloy for Trace Collection
+* ⇢ ⇢ ⇢# OTLP Receiver Configuration
+* ⇢ ⇢ ⇢# Upgrade Alloy
+* ⇢ ⇢ ⇢ Demo Tracing Application
+* ⇢ ⇢ ⇢# Application Architecture
+* ⇢ ⇢ ⇢ Visualizing Traces in Grafana
+* ⇢ ⇢ ⇢# Accessing Traces
+* ⇢ ⇢ ⇢# Service Graph Visualization
+* ⇢ ⇢ ⇢ Correlation Between Observability Signals
+* ⇢ ⇢ ⇢# Traces-to-Logs
+* ⇢ ⇢ ⇢# Traces-to-Metrics
+* ⇢ ⇢ ⇢# Logs-to-Traces
+* ⇢ ⇢ ⇢ Generating Traces for Testing
+* ⇢ ⇢ ⇢ Verifying the Complete Pipeline
+* ⇢ ⇢ ⇢ Practical Example: Viewing a Distributed Trace
+* ⇢ ⇢ ⇢ Storage and Retention
+* ⇢ ⇢ ⇢ Configuration Files
* ⇢ ⇢ Summary
## Introduction
@@ -550,7 +586,7 @@ This file is saved as `freebsd-recording-rules.yaml` and applied as part of the
Unlike memory metrics, disk I/O metrics (`node_disk_read_bytes_total`, `node_disk_written_bytes_total`, etc.) are not available on FreeBSD. The Linux diskstats collector that provides these metrics doesn't have a FreeBSD equivalent in the node_exporter.
-The disk I/O panels in the Node Exporter dashboards will show "No data" for FreeBSD hosts. FreeBSD does expose ZFS-specific metrics (`node_zfs_arcstats_*`) for ARC cache performance, and per-dataset I/O stats are available via `sysctl kstat.zfs`, but mapping these to the Linux-style metrics the dashboards expect is non-trivial. Creating custom ZFS-specific dashboards is left as an exercise for another day.
+The disk I/O panels in the Node Exporter dashboards will show "No data" for FreeBSD hosts. FreeBSD does expose ZFS-specific metrics (`node_zfs_arcstats_*`) for ARC cache performance, and per-dataset I/O stats are available via `sysctl kstat.zfs`, but mapping these to the Linux-style metrics the dashboards expect is non-trivial. Custom ZFS-specific dashboards are covered later in this post.
## Monitoring external OpenBSD hosts
@@ -662,17 +698,1057 @@ This file is saved as `openbsd-recording-rules.yaml` and applied alongside the F
After running `just upgrade`, the OpenBSD hosts appear in Prometheus targets and the Node Exporter dashboards.
+> Updated Mon 09 Mar: Added section about enabling etcd metrics
+
+## Enabling etcd metrics in k3s
+
+The etcd dashboard in Grafana initially showed no data because k3s uses an embedded etcd that doesn't expose metrics by default.
+
+On each control-plane node (r0, r1, r2), create /etc/rancher/k3s/config.yaml:
+
+```
+etcd-expose-metrics: true
+```
+
+Then restart k3s on each node:
+
+```
+systemctl restart k3s
+```
+
+After restarting, etcd metrics are available on port 2381:
+
+```
+curl http://127.0.0.1:2381/metrics | grep etcd
+```
+
+### Configuring Prometheus to scrape etcd
+
+In persistence-values.yaml, enable kubeEtcd with the node IP addresses:
+
+```
+kubeEtcd:
+ enabled: true
+ endpoints:
+ - 192.168.1.120
+ - 192.168.1.121
+ - 192.168.1.122
+ service:
+ enabled: true
+ port: 2381
+ targetPort: 2381
+```
+
+Apply the changes:
+
+```
+just upgrade
+```
+
+### Verifying etcd metrics
+
+After the changes, all etcd targets are being scraped:
+
+```
+kubectl exec -n monitoring prometheus-prometheus-kube-prometheus-prometheus-0 \
+ -c prometheus -- wget -qO- 'http://localhost:9090/api/v1/query?query=etcd_server_has_leader' | \
+ jq -r '.data.result[] | "\(.metric.instance): \(.value[1])"'
+```
+
+Output:
+
+```
+192.168.1.120:2381: 1
+192.168.1.121:2381: 1
+192.168.1.122:2381: 1
+```
+
+The etcd dashboard in Grafana now displays metrics including Raft proposals, leader elections, and peer round trip times.
+
+=> ./f3s-kubernetes-with-freebsd-part-8/grafana-etcd-dashboard.png Grafana etcd dashboard showing cluster health, RPC rate, disk sync duration, and peer round trip times
+
+### Complete persistence-values.yaml
+
+The complete updated persistence-values.yaml:
+
+```
+kubeEtcd:
+ enabled: true
+ endpoints:
+ - 192.168.1.120
+ - 192.168.1.121
+ - 192.168.1.122
+ service:
+ enabled: true
+ port: 2381
+ targetPort: 2381
+
+prometheus:
+ prometheusSpec:
+ additionalScrapeConfigsSecret:
+ enabled: true
+ name: additional-scrape-configs
+ key: additional-scrape-configs.yaml
+ storageSpec:
+ volumeClaimTemplate:
+ spec:
+ storageClassName: ""
+ accessModes: ["ReadWriteOnce"]
+ resources:
+ requests:
+ storage: 10Gi
+ selector:
+ matchLabels:
+ type: local
+ app: prometheus
+
+grafana:
+ persistence:
+ enabled: true
+ type: pvc
+ existingClaim: "grafana-data-pvc"
+
+ initChownData:
+ enabled: false
+
+ podSecurityContext:
+ fsGroup: 911
+ runAsUser: 911
+ runAsGroup: 911
+```
+
+> Updated Mon 09 Mar: Added section about ZFS monitoring for FreeBSD servers
+
+## ZFS Monitoring for FreeBSD Servers
+
+The FreeBSD servers (f0, f1, f2) that provide NFS storage to the k3s cluster have ZFS filesystems. Monitoring ZFS performance is crucial for understanding storage performance and cache efficiency.
+
+### Node Exporter ZFS Collector
+
+The node_exporter running on each FreeBSD server (v1.9.1) includes a built-in ZFS collector that exposes metrics via sysctls. The ZFS collector is enabled by default and provides:
+
+* ARC (Adaptive Replacement Cache) statistics
+* Cache hit/miss rates
+* Memory usage and allocation
+* MRU/MFU cache breakdown
+* Data vs metadata distribution
+
+### Verifying ZFS Metrics
+
+On any FreeBSD server, check that ZFS metrics are being exposed:
+
+```
+paul@f0:~ % curl -s http://localhost:9100/metrics | grep node_zfs_arcstats | wc -l
+ 69
+```
+
+The metrics are automatically scraped by Prometheus through the existing static configuration in additional-scrape-configs.yaml which targets all FreeBSD servers on port 9100 with the os: freebsd label.
+
+### ZFS Recording Rules
+
+Created recording rules for easier dashboard consumption in zfs-recording-rules.yaml:
+
+```
+apiVersion: monitoring.coreos.com/v1
+kind: PrometheusRule
+metadata:
+ name: freebsd-zfs-rules
+ namespace: monitoring
+ labels:
+ release: prometheus
+spec:
+ groups:
+ - name: freebsd-zfs-arc
+ interval: 30s
+ rules:
+ - record: node_zfs_arc_hit_rate_percent
+ expr: |
+ 100 * (
+ rate(node_zfs_arcstats_hits_total{os="freebsd"}[5m]) /
+ (rate(node_zfs_arcstats_hits_total{os="freebsd"}[5m]) +
+ rate(node_zfs_arcstats_misses_total{os="freebsd"}[5m]))
+ )
+ labels:
+ os: freebsd
+ - record: node_zfs_arc_memory_usage_percent
+ expr: |
+ 100 * (
+ node_zfs_arcstats_size_bytes{os="freebsd"} /
+ node_zfs_arcstats_c_max_bytes{os="freebsd"}
+ )
+ labels:
+ os: freebsd
+ # Additional rules for metadata %, target %, MRU/MFU %, etc.
+```
+
+These recording rules calculate:
+
+* ARC hit rate percentage
+* ARC memory usage percentage (current vs maximum)
+* ARC target percentage (target vs maximum)
+* Metadata vs data percentages
+* MRU vs MFU cache percentages
+* Demand data and metadata hit rates
+
+### Grafana Dashboards
+
+Created two comprehensive ZFS monitoring dashboards (zfs-dashboards.yaml):
+
+**Dashboard 1: FreeBSD ZFS (per-host detailed view)**
+
+Includes variables to select:
+
+* FreeBSD server (f0, f1, or f2)
+* ZFS pool (zdata, zroot, or all)
+
+Pool Overview Row:
+
+* Pool Capacity gauge (with thresholds: green <70%, yellow <85%, red >85%)
+* Pool Health status (ONLINE/DEGRADED/FAULTED with color coding)
+* Total Pool Size stat
+* Free Space stat
+* Pool Space Usage Over Time (stacked: used + free)
+* Pool Capacity Trend time series
+
+Dataset Statistics Row:
+
+* Table showing all datasets with columns: Pool, Dataset, Used, Available, Referenced
+* Automatically filters by selected pool
+
+ARC Cache Statistics Row:
+
+* ARC Hit Rate gauge (red <70%, yellow <90%, green >=90%)
+* ARC Size time series (current, target, max)
+* ARC Memory Usage percentage gauge
+* ARC Hits vs Misses rate
+* ARC Data vs Metadata stacked time series
+
+**Dashboard 2: FreeBSD ZFS Summary (cluster-wide overview)**
+
+Cluster-Wide Pool Statistics Row:
+
+* Total Storage Capacity across all servers
+* Total Used space
+* Total Free space
+* Average Pool Capacity gauge
+* Pool Health Status (worst case across cluster)
+* Total Pool Space Usage Over Time
+* Per-Pool Capacity time series (all pools on all hosts)
+
+Per-Host Pool Breakdown Row:
+
+* Bar gauge showing capacity by host and pool
+* Table with all pools: Host, Pool, Size, Used, Free, Capacity %, Health
+
+Cluster-Wide ARC Statistics Row:
+
+* Average ARC Hit Rate gauge across all hosts
+* ARC Hit Rate by Host time series
+* Total ARC Size Across Cluster
+* Total ARC Hits vs Misses (cluster-wide sum)
+* ARC Size by Host
+
+Dashboard Visualization:
+
+=> ./f3s-kubernetes-with-freebsd-part-8/grafana-zfs-dashboard.png ZFS monitoring dashboard in Grafana showing pool capacity, health, and I/O throughput
+=> ./f3s-kubernetes-with-freebsd-part-8/grafana-zfs-arc-stats.png ZFS ARC cache statistics showing hit rate, memory usage, and size trends
+=> ./f3s-kubernetes-with-freebsd-part-8/grafana-zfs-datasets.png ZFS datasets table and ARC data vs metadata breakdown
+
+### Deployment
+
+Applied the resources to the cluster:
+
+```
+cd /home/paul/git/conf/f3s/prometheus
+kubectl apply -f zfs-recording-rules.yaml
+kubectl apply -f zfs-dashboards.yaml
+```
+
+Updated Justfile to include ZFS recording rules in install and upgrade targets:
+
+```
+install:
+ kubectl apply -f persistent-volumes.yaml
+ kubectl create secret generic additional-scrape-configs --from-file=additional-scrape-configs.yaml -n monitoring --dry-run=client -o yaml | kubectl apply -f -
+ helm install prometheus prometheus-community/kube-prometheus-stack --namespace monitoring -f persistence-values.yaml
+ kubectl apply -f freebsd-recording-rules.yaml
+ kubectl apply -f openbsd-recording-rules.yaml
+ kubectl apply -f zfs-recording-rules.yaml
+ just -f grafana-ingress/Justfile install
+```
+
+### Verifying ZFS Metrics in Prometheus
+
+Check that ZFS metrics are being collected:
+
+```
+kubectl exec -n monitoring prometheus-prometheus-kube-prometheus-prometheus-0 -c prometheus -- \
+ wget -qO- 'http://localhost:9090/api/v1/query?query=node_zfs_arcstats_size_bytes'
+```
+
+Check recording rules are calculating correctly:
+
+```
+kubectl exec -n monitoring prometheus-prometheus-kube-prometheus-prometheus-0 -c prometheus -- \
+ wget -qO- 'http://localhost:9090/api/v1/query?query=node_zfs_arc_memory_usage_percent'
+```
+
+Example output shows memory usage percentage for each FreeBSD server:
+
+```
+"result":[
+ {"metric":{"instance":"192.168.2.130:9100","os":"freebsd"},"value":[...,"37.58"]},
+ {"metric":{"instance":"192.168.2.131:9100","os":"freebsd"},"value":[...,"12.85"]},
+ {"metric":{"instance":"192.168.2.132:9100","os":"freebsd"},"value":[...,"13.44"]}
+]
+```
+
+### Key Metrics to Monitor
+
+* ARC Hit Rate: Should typically be above 90% for optimal performance. Lower hit rates indicate the ARC cache is too small or workload has poor locality.
+* ARC Memory Usage: Shows how much of the maximum ARC size is being used. If consistently at or near maximum, the ARC is effectively utilizing available memory.
+* Data vs Metadata: Typically data should dominate, but workloads with many small files will show higher metadata percentages.
+* MRU vs MFU: Most Recently Used vs Most Frequently Used cache. The ratio depends on workload characteristics.
+* Pool Capacity: Monitor pool usage to ensure adequate free space. ZFS performance degrades when pools exceed 80% capacity.
+* Pool Health: Should always show ONLINE (green). DEGRADED (yellow) indicates a disk issue requiring attention. FAULTED (red) requires immediate action.
+* Dataset Usage: Track which datasets are consuming the most space to identify growth trends and plan capacity.
+
+### ZFS Pool and Dataset Metrics via Textfile Collector
+
+To complement the ARC statistics from node_exporter's built-in ZFS collector, I added pool capacity and dataset metrics using the textfile collector feature.
+
+Created a script at `/usr/local/bin/zfs_pool_metrics.sh` on each FreeBSD server:
+
+```
+#!/bin/sh
+# ZFS Pool and Dataset Metrics Collector for Prometheus
+
+OUTPUT_FILE="/var/tmp/node_exporter/zfs_pools.prom.$$"
+FINAL_FILE="/var/tmp/node_exporter/zfs_pools.prom"
+
+mkdir -p /var/tmp/node_exporter
+
+{
+ # Pool metrics
+ echo "# HELP zfs_pool_size_bytes Total size of ZFS pool"
+ echo "# TYPE zfs_pool_size_bytes gauge"
+ echo "# HELP zfs_pool_allocated_bytes Allocated space in ZFS pool"
+ echo "# TYPE zfs_pool_allocated_bytes gauge"
+ echo "# HELP zfs_pool_free_bytes Free space in ZFS pool"
+ echo "# TYPE zfs_pool_free_bytes gauge"
+ echo "# HELP zfs_pool_capacity_percent Capacity percentage"
+ echo "# TYPE zfs_pool_capacity_percent gauge"
+ echo "# HELP zfs_pool_health Pool health (0=ONLINE, 1=DEGRADED, 2=FAULTED)"
+ echo "# TYPE zfs_pool_health gauge"
+
+ zpool list -Hp -o name,size,allocated,free,capacity,health | \
+ while IFS=$'\t' read name size alloc free cap health; do
+ case "$health" in
+ ONLINE) health_val=0 ;;
+ DEGRADED) health_val=1 ;;
+ FAULTED) health_val=2 ;;
+ *) health_val=6 ;;
+ esac
+ cap_num=$(echo "$cap" | sed 's/%//')
+
+ echo "zfs_pool_size_bytes{pool=\"$name\"} $size"
+ echo "zfs_pool_allocated_bytes{pool=\"$name\"} $alloc"
+ echo "zfs_pool_free_bytes{pool=\"$name\"} $free"
+ echo "zfs_pool_capacity_percent{pool=\"$name\"} $cap_num"
+ echo "zfs_pool_health{pool=\"$name\"} $health_val"
+ done
+
+ # Dataset metrics
+ echo "# HELP zfs_dataset_used_bytes Used space in dataset"
+ echo "# TYPE zfs_dataset_used_bytes gauge"
+ echo "# HELP zfs_dataset_available_bytes Available space"
+ echo "# TYPE zfs_dataset_available_bytes gauge"
+ echo "# HELP zfs_dataset_referenced_bytes Referenced space"
+ echo "# TYPE zfs_dataset_referenced_bytes gauge"
+
+ zfs list -Hp -t filesystem -o name,used,available,referenced | \
+ while IFS=$'\t' read name used avail ref; do
+ pool=$(echo "$name" | cut -d/ -f1)
+ echo "zfs_dataset_used_bytes{pool=\"$pool\",dataset=\"$name\"} $used"
+ echo "zfs_dataset_available_bytes{pool=\"$pool\",dataset=\"$name\"} $avail"
+ echo "zfs_dataset_referenced_bytes{pool=\"$pool\",dataset=\"$name\"} $ref"
+ done
+} > "$OUTPUT_FILE"
+
+mv "$OUTPUT_FILE" "$FINAL_FILE"
+```
+
+Deployed to all FreeBSD servers:
+
+```
+for host in f0 f1 f2; do
+ scp /tmp/zfs_pool_metrics.sh paul@$host:/tmp/
+ ssh paul@$host 'doas mv /tmp/zfs_pool_metrics.sh /usr/local/bin/ && \
+ doas chmod +x /usr/local/bin/zfs_pool_metrics.sh'
+done
+```
+
+Set up cron jobs to run every minute:
+
+```
+for host in f0 f1 f2; do
+ ssh paul@$host 'echo "* * * * * /usr/local/bin/zfs_pool_metrics.sh >/dev/null 2>&1" | \
+ doas crontab -'
+done
+```
+
+The textfile collector (already configured with --collector.textfile.directory=/var/tmp/node_exporter) automatically picks up the metrics.
+
+Verify metrics are being exposed:
+
+```
+paul@f0:~ % curl -s http://localhost:9100/metrics | grep "^zfs_pool" | head -5
+zfs_pool_allocated_bytes{pool="zdata"} 6.47622733824e+11
+zfs_pool_allocated_bytes{pool="zroot"} 5.3338578944e+10
+zfs_pool_capacity_percent{pool="zdata"} 64
+zfs_pool_capacity_percent{pool="zroot"} 10
+zfs_pool_free_bytes{pool="zdata"} 3.48809678848e+11
+```
+
+> Updated Mon 09 Mar: Added section about distributed tracing with Grafana Tempo
+
+## Distributed Tracing with Grafana Tempo
+
+After implementing logs (Loki) and metrics (Prometheus), the final pillar of observability is distributed tracing. Grafana Tempo provides distributed tracing capabilities that help understand request flows across microservices.
+
+How will this look tracing with Tempo like in Grafana? Have a look at the X-RAG blog post of mine:
+
+=> ./2025-12-24-x-rag-observability-hackathon.gmi X-RAG Observability Hackathon
+
+### Why Distributed Tracing?
+
+In a microservices architecture, a single user request may traverse multiple services. Distributed tracing:
+
+* Tracks requests across service boundaries
+* Identifies performance bottlenecks
+* Visualizes service dependencies
+* Correlates with logs and metrics
+* Helps debug complex distributed systems
+
+### Deploying Grafana Tempo
+
+Tempo is deployed in monolithic mode, following the same pattern as Loki's SingleBinary deployment.
+
+#### Configuration Strategy
+
+**Deployment Mode:** Monolithic (all components in one process)
+* Simpler operation than microservices mode
+* Suitable for the cluster scale
+* Consistent with Loki deployment pattern
+
+**Storage:** Filesystem backend using hostPath
+* 10Gi storage at /data/nfs/k3svolumes/tempo/data
+* 7-day retention (168h)
+* Local storage is the only option for monolithic mode
+
+**OTLP Receivers:** Standard OpenTelemetry Protocol ports
+* gRPC: 4317
+* HTTP: 4318
+* Bind to 0.0.0.0 to avoid Tempo 2.7+ localhost-only binding issue
+
+#### Tempo Deployment Files
+
+Created in /home/paul/git/conf/f3s/tempo/:
+
+**values.yaml** - Helm chart configuration:
+
+```
+tempo:
+ retention: 168h
+ storage:
+ trace:
+ backend: local
+ local:
+ path: /var/tempo/traces
+ wal:
+ path: /var/tempo/wal
+ receivers:
+ otlp:
+ protocols:
+ grpc:
+ endpoint: 0.0.0.0:4317
+ http:
+ endpoint: 0.0.0.0:4318
+
+persistence:
+ enabled: true
+ size: 10Gi
+ storageClassName: ""
+
+resources:
+ limits:
+ cpu: 1000m
+ memory: 2Gi
+ requests:
+ cpu: 500m
+ memory: 1Gi
+```
+
+**persistent-volumes.yaml** - Storage configuration:
+
+```
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: tempo-data-pv
+spec:
+ capacity:
+ storage: 10Gi
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ hostPath:
+ path: /data/nfs/k3svolumes/tempo/data
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: tempo-data-pvc
+ namespace: monitoring
+spec:
+ storageClassName: ""
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
+```
+
+**Grafana Datasource Provisioning**
+
+All Grafana datasources (Prometheus, Alertmanager, Loki, Tempo) are provisioned via a unified ConfigMap that is directly mounted to the Grafana pod. This approach ensures datasources are loaded on startup without requiring sidecar-based discovery.
+
+In /home/paul/git/conf/f3s/prometheus/grafana-datasources-all.yaml:
+
+```
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: grafana-datasources-all
+ namespace: monitoring
+data:
+ datasources.yaml: |
+ apiVersion: 1
+ datasources:
+ - name: Prometheus
+ type: prometheus
+ uid: prometheus
+ url: http://prometheus-kube-prometheus-prometheus.monitoring:9090/
+ access: proxy
+ isDefault: true
+ - name: Alertmanager
+ type: alertmanager
+ uid: alertmanager
+ url: http://prometheus-kube-prometheus-alertmanager.monitoring:9093/
+ - name: Loki
+ type: loki
+ uid: loki
+ url: http://loki.monitoring.svc.cluster.local:3100
+ - name: Tempo
+ type: tempo
+ uid: tempo
+ url: http://tempo.monitoring.svc.cluster.local:3200
+ jsonData:
+ tracesToLogsV2:
+ datasourceUid: loki
+ spanStartTimeShift: -1h
+ spanEndTimeShift: 1h
+ tracesToMetrics:
+ datasourceUid: prometheus
+ serviceMap:
+ datasourceUid: prometheus
+ nodeGraph:
+ enabled: true
+```
+
+The kube-prometheus-stack Helm values (persistence-values.yaml) are configured to:
+* Disable sidecar-based datasource provisioning
+* Mount grafana-datasources-all ConfigMap directly to /etc/grafana/provisioning/datasources/
+
+This direct mounting approach is simpler and more reliable than sidecar-based discovery.
+
+#### Installation
+
+```
+cd /home/paul/git/conf/f3s/tempo
+just install
+```
+
+Verify Tempo is running:
+
+```
+kubectl get pods -n monitoring -l app.kubernetes.io/name=tempo
+kubectl exec -n monitoring <tempo-pod> -- wget -qO- http://localhost:3200/ready
+```
+
+### Configuring Grafana Alloy for Trace Collection
+
+Updated /home/paul/git/conf/f3s/loki/alloy-values.yaml to add OTLP receivers for traces while maintaining existing log collection.
+
+#### OTLP Receiver Configuration
+
+Added to Alloy configuration after the log collection pipeline:
+
+```
+// OTLP receiver for traces via gRPC and HTTP
+otelcol.receiver.otlp "default" {
+ grpc {
+ endpoint = "0.0.0.0:4317"
+ }
+ http {
+ endpoint = "0.0.0.0:4318"
+ }
+ output {
+ traces = [otelcol.processor.batch.default.input]
+ }
+}
+
+// Batch processor for efficient trace forwarding
+otelcol.processor.batch "default" {
+ timeout = "5s"
+ send_batch_size = 100
+ send_batch_max_size = 200
+ output {
+ traces = [otelcol.exporter.otlp.tempo.input]
+ }
+}
+
+// OTLP exporter to send traces to Tempo
+otelcol.exporter.otlp "tempo" {
+ client {
+ endpoint = "tempo.monitoring.svc.cluster.local:4317"
+ tls {
+ insecure = true
+ }
+ compression = "gzip"
+ }
+}
+```
+
+The batch processor reduces network overhead by accumulating spans before forwarding to Tempo.
+
+#### Upgrade Alloy
+
+```
+cd /home/paul/git/conf/f3s/loki
+just upgrade
+```
+
+Verify OTLP receivers are listening:
+
+```
+kubectl logs -n monitoring -l app.kubernetes.io/name=alloy | grep -i "otlp.*receiver"
+kubectl exec -n monitoring <alloy-pod> -- netstat -ln | grep -E ':(4317|4318)'
+```
+
+### Demo Tracing Application
+
+Created a three-tier Python application to demonstrate distributed tracing in action.
+
+#### Application Architecture
+
+```
+User → Frontend (Flask:5000) → Middleware (Flask:5001) → Backend (Flask:5002)
+ ↓ ↓ ↓
+ Alloy (OTLP:4317) → Tempo → Grafana
+```
+
+Frontend Service:
+
+* Receives HTTP requests at /api/process
+* Forwards to middleware service
+* Creates parent span for the entire request
+
+Middleware Service:
+
+* Transforms data at /api/transform
+* Calls backend service
+* Creates child span linked to frontend
+
+Backend Service:
+
+* Returns data at /api/data
+* Simulates database query (100ms sleep)
+* Creates leaf span in the trace
+
+OpenTelemetry Instrumentation:
+
+All services use Python OpenTelemetry libraries:
+
+**Dependencies:**
+```
+flask==3.0.0
+requests==2.31.0
+opentelemetry-distro==0.49b0
+opentelemetry-exporter-otlp==1.28.0
+opentelemetry-instrumentation-flask==0.49b0
+opentelemetry-instrumentation-requests==0.49b0
+```
+
+**Auto-instrumentation pattern** (used in all services):
+
+```python
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+from opentelemetry.instrumentation.flask import FlaskInstrumentor
+from opentelemetry.instrumentation.requests import RequestsInstrumentor
+from opentelemetry.sdk.resources import Resource
+
+# Define service identity
+resource = Resource(attributes={
+ "service.name": "frontend",
+ "service.namespace": "tracing-demo",
+ "service.version": "1.0.0"
+})
+
+provider = TracerProvider(resource=resource)
+
+# Export to Alloy
+otlp_exporter = OTLPSpanExporter(
+ endpoint="http://alloy.monitoring.svc.cluster.local:4317",
+ insecure=True
+)
+
+processor = BatchSpanProcessor(otlp_exporter)
+provider.add_span_processor(processor)
+trace.set_tracer_provider(provider)
+
+# Auto-instrument Flask and requests
+FlaskInstrumentor().instrument_app(app)
+RequestsInstrumentor().instrument()
+```
+
+The auto-instrumentation automatically:
+* Creates spans for HTTP requests
+* Propagates trace context via W3C Trace Context headers
+* Links parent and child spans across service boundaries
+
+Deployment:
+
+Created Helm chart in /home/paul/git/conf/f3s/tracing-demo/ with three separate deployments, services, and an ingress.
+
+Build and deploy:
+
+```
+cd /home/paul/git/conf/f3s/tracing-demo
+just build
+just import
+just install
+```
+
+Verify deployment:
+
+```
+kubectl get pods -n services | grep tracing-demo
+kubectl get ingress -n services tracing-demo-ingress
+```
+
+Access the application at:
+
+=> http://tracing-demo.f3s.buetow.org
+
+### Visualizing Traces in Grafana
+
+The Tempo datasource is automatically discovered by Grafana through the ConfigMap label.
+
+#### Accessing Traces
+
+Navigate to Grafana → Explore → Select "Tempo" datasource
+
+**Search Interface:**
+* Search by Trace ID
+* Search by service name
+* Search by tags
+
+**TraceQL Queries:**
+
+Find all traces from demo app:
+```
+{ resource.service.namespace = "tracing-demo" }
+```
+
+Find slow requests (>200ms):
+```
+{ duration > 200ms }
+```
+
+Find traces from specific service:
+```
+{ resource.service.name = "frontend" }
+```
+
+Find errors:
+```
+{ status = error }
+```
+
+Complex query - frontend traces calling middleware:
+```
+{ resource.service.namespace = "tracing-demo" } && { span.http.status_code >= 500 }
+```
+
+#### Service Graph Visualization
+
+The service graph shows visual connections between services:
+
+1. Navigate to Explore → Tempo
+2. Enable "Service Graph" view
+3. Shows: Frontend → Middleware → Backend with request rates
+
+The service graph uses Prometheus metrics generated from trace data.
+
+### Correlation Between Observability Signals
+
+Tempo integrates with Loki and Prometheus to provide unified observability.
+
+#### Traces-to-Logs
+
+Click on any span in a trace to see related logs:
+
+1. View trace in Grafana
+2. Click on a span
+3. Select "Logs for this span"
+4. Loki shows logs filtered by:
+ * Time range (span duration ± 1 hour)
+ * Service name
+ * Namespace
+ * Pod
+
+This helps correlate what the service was doing when the span was created.
+
+#### Traces-to-Metrics
+
+View Prometheus metrics for services in the trace:
+
+1. View trace in Grafana
+2. Select "Metrics" tab
+3. Shows metrics like:
+ * Request rate
+ * Error rate
+ * Duration percentiles
+
+#### Logs-to-Traces
+
+From logs, you can jump to related traces:
+
+1. In Loki, logs that contain trace IDs are automatically linked
+2. Click the trace ID to view the full trace
+3. See the complete request flow
+
+### Generating Traces for Testing
+
+Test the demo application:
+
+```
+curl http://tracing-demo.f3s.buetow.org/api/process
+```
+
+Load test (generates 50 traces):
+
+```
+cd /home/paul/git/conf/f3s/tracing-demo
+just load-test
+```
+
+Each request creates a distributed trace spanning all three services.
+
+### Verifying the Complete Pipeline
+
+Check the trace flow end-to-end:
+
+**1. Application generates traces:**
+```
+kubectl logs -n services -l app=tracing-demo-frontend | grep -i trace
+```
+
+**2. Alloy receives traces:**
+```
+kubectl logs -n monitoring -l app.kubernetes.io/name=alloy | grep -i otlp
+```
+
+**3. Tempo stores traces:**
+```
+kubectl logs -n monitoring -l app.kubernetes.io/name=tempo | grep -i trace
+```
+
+**4. Grafana displays traces:**
+Navigate to Explore → Tempo → Search for traces
+
+### Practical Example: Viewing a Distributed Trace
+
+Let's generate a trace and examine it in Grafana.
+
+**1. Generate a trace by calling the demo application:**
+
+```
+curl -H "Host: tracing-demo.f3s.buetow.org" http://r0/api/process
+```
+
+**Response (HTTP 200):**
+
+```json
+{
+ "middleware_response": {
+ "backend_data": {
+ "data": {
+ "id": 12345,
+ "query_time_ms": 100.0,
+ "timestamp": "2025-12-28T18:35:01.064538",
+ "value": "Sample data from backend service"
+ },
+ "service": "backend"
+ },
+ "middleware_processed": true,
+ "original_data": {
+ "source": "GET request"
+ },
+ "transformation_time_ms": 50
+ },
+ "request_data": {
+ "source": "GET request"
+ },
+ "service": "frontend",
+ "status": "success"
+}
+```
+
+**2. Find the trace in Tempo via API:**
+
+After a few seconds (for batch export), search for recent traces:
+
+```
+kubectl exec -n monitoring tempo-0 -- wget -qO- \
+ 'http://localhost:3200/api/search?tags=service.namespace%3Dtracing-demo&limit=5' 2>/dev/null | \
+ python3 -m json.tool
+```
+
+Returns traces including:
+
+```json
+{
+ "traceID": "4be1151c0bdcd5625ac7e02b98d95bd5",
+ "rootServiceName": "frontend",
+ "rootTraceName": "GET /api/process",
+ "durationMs": 221
+}
+```
+
+**3. Fetch complete trace details:**
+
+```
+kubectl exec -n monitoring tempo-0 -- wget -qO- \
+ 'http://localhost:3200/api/traces/4be1151c0bdcd5625ac7e02b98d95bd5' 2>/dev/null | \
+ python3 -m json.tool
+```
+
+**Trace structure (8 spans across 3 services):**
+
+```
+Trace ID: 4be1151c0bdcd5625ac7e02b98d95bd5
+Services: 3 (frontend, middleware, backend)
+
+Service: frontend
+ └─ GET /api/process 221.10ms (HTTP server span)
+ └─ frontend-process 216.23ms (custom business logic span)
+ └─ POST 209.97ms (HTTP client span to middleware)
+
+Service: middleware
+ └─ POST /api/transform 186.02ms (HTTP server span)
+ └─ middleware-transform 180.96ms (custom business logic span)
+ └─ GET 127.52ms (HTTP client span to backend)
+
+Service: backend
+ └─ GET /api/data 103.93ms (HTTP server span)
+ └─ backend-get-data 102.11ms (custom business logic span with 100ms sleep)
+```
+
+**4. View the trace in Grafana UI:**
+
+Navigate to: Grafana → Explore → Tempo datasource
+
+Search using TraceQL:
+```
+{ resource.service.namespace = "tracing-demo" }
+```
+
+Or directly open the trace by pasting the trace ID in the search box:
+```
+4be1151c0bdcd5625ac7e02b98d95bd5
+```
+
+**5. Trace visualization:**
+
+The trace waterfall view in Grafana shows the complete request flow with timing:
+
+=> ./f3s-kubernetes-with-freebsd-part-8/grafana-tempo-trace.png Distributed trace visualization in Grafana Tempo showing Frontend → Middleware → Backend spans
+
+For additional examples of Tempo trace visualization, see also:
+
+=> https://foo.zone/gemfeed/2025-12-24-x-rag-observability-hackathon.html X-RAG Observability Hackathon (more Grafana Tempo screenshots)
+
+The trace reveals the distributed request flow:
+
+* Frontend (221ms): Receives GET /api/process, executes business logic, calls middleware
+* Middleware (186ms): Receives POST /api/transform, transforms data, calls backend
+* Backend (104ms): Receives GET /api/data, simulates database query with 100ms sleep
+* Total request time: 221ms end-to-end
+* Span propagation: W3C Trace Context headers automatically link all spans
+
+**6. Service graph visualization:**
+
+The service graph is automatically generated from traces and shows service dependencies. For examples of service graph visualization in Grafana, see the screenshots in the X-RAG Observability Hackathon blog post.
+
+=> ./2025-12-24-x-rag-observability-hackathon.gmi X-RAG Observability Hackathon (includes service graph screenshots)
+
+This visualization helps identify:
+
+* Request rates between services
+* Average latency for each hop
+* Error rates (if any)
+* Service dependencies and communication patterns
+
+### Storage and Retention
+
+Monitor Tempo storage usage:
+
+```
+kubectl exec -n monitoring <tempo-pod> -- df -h /var/tempo
+```
+
+With 10Gi storage and 7-day retention, the system handles moderate trace volumes. If storage fills up:
+
+* Reduce retention to 72h (3 days)
+* Implement sampling in Alloy
+* Increase PV size
+
+### Configuration Files
+
+All configuration files are available on Codeberg:
+
+=> https://codeberg.org/snonux/conf/src/branch/master/f3s/tempo Tempo configuration
+=> https://codeberg.org/snonux/conf/src/branch/master/f3s/loki Alloy configuration (updated for traces)
+=> https://codeberg.org/snonux/conf/src/branch/master/f3s/tracing-demo Demo tracing application
+
## Summary
-With Prometheus, Grafana, Loki, and Alloy deployed, I now have complete visibility into the k3s cluster, the FreeBSD storage servers, and the OpenBSD edge relays:
+With Prometheus, Grafana, Loki, Alloy, and Tempo deployed, I now have complete visibility into the k3s cluster, the FreeBSD storage servers, and the OpenBSD edge relays:
-* metrics: Prometheus collects and stores time-series data from all components
+* Metric