summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--f3s/loki/alloy-values.yaml67
-rw-r--r--f3s/prometheus/persistence-values.yaml30
-rw-r--r--f3s/tempo/Justfile33
-rw-r--r--f3s/tempo/README.md182
-rw-r--r--f3s/tempo/datasource-configmap.yaml47
-rw-r--r--f3s/tempo/persistent-volumes.yaml31
-rw-r--r--f3s/tempo/values.yaml76
-rw-r--r--f3s/tracing-demo/Justfile93
-rw-r--r--f3s/tracing-demo/README.md250
-rw-r--r--f3s/tracing-demo/docker-image-Justfile38
-rw-r--r--f3s/tracing-demo/docker/backend/Dockerfile16
-rw-r--r--f3s/tracing-demo/docker/backend/app.py115
-rw-r--r--f3s/tracing-demo/docker/backend/requirements.txt4
-rw-r--r--f3s/tracing-demo/docker/frontend/Dockerfile16
-rw-r--r--f3s/tracing-demo/docker/frontend/app.py149
-rw-r--r--f3s/tracing-demo/docker/frontend/requirements.txt6
-rw-r--r--f3s/tracing-demo/docker/middleware/Dockerfile16
-rw-r--r--f3s/tracing-demo/docker/middleware/app.py147
-rw-r--r--f3s/tracing-demo/docker/middleware/requirements.txt6
-rw-r--r--f3s/tracing-demo/helm-chart/Chart.yaml5
-rw-r--r--f3s/tracing-demo/helm-chart/templates/backend-deployment.yaml51
-rw-r--r--f3s/tracing-demo/helm-chart/templates/backend-service.yaml17
-rw-r--r--f3s/tracing-demo/helm-chart/templates/frontend-deployment.yaml53
-rw-r--r--f3s/tracing-demo/helm-chart/templates/frontend-service.yaml17
-rw-r--r--f3s/tracing-demo/helm-chart/templates/ingress.yaml22
-rw-r--r--f3s/tracing-demo/helm-chart/templates/middleware-deployment.yaml53
-rw-r--r--f3s/tracing-demo/helm-chart/templates/middleware-service.yaml17
27 files changed, 1556 insertions, 1 deletions
diff --git a/f3s/loki/alloy-values.yaml b/f3s/loki/alloy-values.yaml
index 09da220..f53fd12 100644
--- a/f3s/loki/alloy-values.yaml
+++ b/f3s/loki/alloy-values.yaml
@@ -1,4 +1,17 @@
alloy:
+ service:
+ ports:
+ otlp-grpc:
+ enabled: true
+ port: 4317
+ targetPort: 4317
+ protocol: TCP
+ otlp-http:
+ enabled: true
+ port: 4318
+ targetPort: 4318
+ protocol: TCP
+
configMap:
content: |
discovery.kubernetes "pods" {
@@ -39,3 +52,57 @@ alloy:
url = "http://loki.monitoring.svc.cluster.local:3100/loki/api/v1/push"
}
}
+
+ // ========================================
+ // TRACES COLLECTION
+ // ========================================
+
+ // OTLP receiver for traces via gRPC and HTTP
+ // Accepts traces from applications instrumented with OpenTelemetry
+ otelcol.receiver.otlp "default" {
+ // Accept OTLP over gRPC on port 4317 (standard OTLP port)
+ grpc {
+ endpoint = "0.0.0.0:4317"
+ }
+
+ // Accept OTLP over HTTP on port 4318 (standard OTLP HTTP port)
+ http {
+ endpoint = "0.0.0.0:4318"
+ }
+
+ output {
+ traces = [otelcol.processor.batch.default.input]
+ }
+ }
+
+ // Batch processor for efficient trace forwarding to Tempo
+ // Reduces network calls by batching spans before sending
+ otelcol.processor.batch "default" {
+ // Send batch every 5 seconds
+ timeout = "5s"
+
+ // Or when 100 spans have accumulated
+ send_batch_size = 100
+
+ // Maximum batch size as safety limit
+ 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"
+
+ // Tempo doesn't use TLS for internal cluster communication
+ tls {
+ insecure = true
+ }
+
+ // Enable compression for efficiency
+ compression = "gzip"
+ }
+ }
diff --git a/f3s/prometheus/persistence-values.yaml b/f3s/prometheus/persistence-values.yaml
index b5e13e4..7e115a9 100644
--- a/f3s/prometheus/persistence-values.yaml
+++ b/f3s/prometheus/persistence-values.yaml
@@ -55,4 +55,32 @@ grafana:
podSecurityContext:
fsGroup: 911
runAsUser: 911
- runAsGroup: 911 \ No newline at end of file
+ runAsGroup: 911
+
+ additionalDataSources:
+ - name: Tempo
+ type: tempo
+ uid: tempo
+ url: http://tempo.monitoring.svc.cluster.local:3200
+ access: proxy
+ isDefault: false
+ editable: true
+ jsonData:
+ httpMethod: GET
+ tracesToLogsV2:
+ datasourceUid: 'loki'
+ spanStartTimeShift: '-1h'
+ spanEndTimeShift: '1h'
+ filterByTraceID: false
+ filterBySpanID: false
+ tags: ['cluster', 'namespace', 'pod', 'app']
+ tracesToMetrics:
+ datasourceUid: 'prometheus'
+ serviceMap:
+ datasourceUid: 'prometheus'
+ nodeGraph:
+ enabled: true
+ search:
+ hide: false
+ lokiSearch:
+ datasourceUid: 'loki' \ No newline at end of file
diff --git a/f3s/tempo/Justfile b/f3s/tempo/Justfile
new file mode 100644
index 0000000..361f5c6
--- /dev/null
+++ b/f3s/tempo/Justfile
@@ -0,0 +1,33 @@
+# Grafana Tempo deployment automation
+# Following the pattern from Loki Justfile
+
+install:
+ helm repo add grafana https://grafana.github.io/helm-charts || true
+ helm repo update
+ kubectl apply -f persistent-volumes.yaml
+ helm install tempo grafana/tempo --namespace monitoring -f values.yaml
+ kubectl apply -f datasource-configmap.yaml
+
+uninstall:
+ kubectl delete -f datasource-configmap.yaml || true
+ helm uninstall tempo --namespace monitoring || true
+ kubectl delete -f persistent-volumes.yaml || true
+
+upgrade:
+ helm upgrade tempo grafana/tempo --namespace monitoring -f values.yaml
+ kubectl apply -f datasource-configmap.yaml
+
+status:
+ kubectl get pods -n monitoring -l app.kubernetes.io/name=tempo
+ kubectl get svc -n monitoring -l app.kubernetes.io/name=tempo
+ kubectl get pvc -n monitoring tempo-data-pvc
+
+logs:
+ kubectl logs -n monitoring -l app.kubernetes.io/name=tempo --tail=100 -f
+
+check:
+ @echo "Checking Tempo readiness..."
+ kubectl exec -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=tempo -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://localhost:3200/ready
+ @echo ""
+ @echo "Checking OTLP ports..."
+ kubectl exec -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=tempo -o jsonpath='{.items[0].metadata.name}') -- netstat -ln | grep -E ':(4317|4318|3200)'
diff --git a/f3s/tempo/README.md b/f3s/tempo/README.md
new file mode 100644
index 0000000..ee59311
--- /dev/null
+++ b/f3s/tempo/README.md
@@ -0,0 +1,182 @@
+# Grafana Tempo - Distributed Tracing
+
+Grafana Tempo deployment for the f3s Kubernetes cluster in monolithic mode.
+
+## Overview
+
+- **Deployment Mode**: Monolithic (all components in one process)
+- **Storage Backend**: Filesystem (local storage on hostPath)
+- **Storage Size**: 10Gi
+- **Retention**: 7 days (168h)
+- **Namespace**: `monitoring`
+
+## Components
+
+- **Tempo**: Distributed tracing backend
+- **OTLP Receivers**: Accepts traces via gRPC (4317) and HTTP (4318)
+- **Query Frontend**: Query interface on port 3200
+- **Grafana Datasource**: Auto-discovered via ConfigMap label
+
+## Architecture
+
+```
+Applications → Alloy (OTLP collector) → Tempo → Grafana
+```
+
+## Installation
+
+```bash
+just install
+```
+
+This will:
+1. Add Grafana Helm repo and update
+2. Create PersistentVolume and PersistentVolumeClaim
+3. Install Tempo via Helm
+4. Create Grafana datasource ConfigMap
+
+## Configuration
+
+### values.yaml
+
+- Monolithic mode configuration
+- OTLP receivers on ports 4317 (gRPC) and 4318 (HTTP)
+- Local filesystem storage at `/var/tempo/traces`
+- Resource limits: 2Gi memory, 1 CPU
+
+### persistent-volumes.yaml
+
+- PV: `tempo-data-pv` at `/data/nfs/k3svolumes/tempo/data`
+- PVC: `tempo-data-pvc` (10Gi, ReadWriteOnce)
+
+### datasource-configmap.yaml
+
+- Auto-discovered by Grafana sidecar
+- Enables traces-to-logs correlation with Loki
+- Enables traces-to-metrics correlation with Prometheus
+- Enables service graph visualization
+
+## Grafana Integration
+
+The datasource is automatically discovered by Grafana through the ConfigMap with label `grafana_datasource: "1"`.
+
+To access traces in Grafana:
+1. Navigate to Explore
+2. Select "Tempo" datasource
+3. Use Search or TraceQL queries
+
+### Example TraceQL Queries
+
+```
+# Find all traces from demo app
+{ resource.service.namespace = "tracing-demo" }
+
+# Find slow requests (>200ms)
+{ duration > 200ms }
+
+# Find errors
+{ status = error }
+
+# Find traces from specific service
+{ resource.service.name = "frontend" }
+```
+
+## Verification
+
+Check that Tempo is running:
+```bash
+just status
+```
+
+Check Tempo readiness and OTLP ports:
+```bash
+just check
+```
+
+View logs:
+```bash
+just logs
+```
+
+## Sending Traces
+
+Applications should send traces to Alloy's OTLP receivers:
+- gRPC: `alloy.monitoring.svc.cluster.local:4317`
+- HTTP: `alloy.monitoring.svc.cluster.local:4318`
+
+Alloy forwards traces to Tempo at `tempo.monitoring.svc.cluster.local:4317`.
+
+## Maintenance
+
+### Upgrade
+
+```bash
+just upgrade
+```
+
+### Uninstall
+
+```bash
+just uninstall
+```
+
+### Check Storage Usage
+
+```bash
+kubectl exec -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=tempo -o jsonpath='{.items[0].metadata.name}') -- df -h /var/tempo
+```
+
+## Troubleshooting
+
+### Tempo pod not starting
+
+Check events:
+```bash
+kubectl describe pod -n monitoring -l app.kubernetes.io/name=tempo
+```
+
+Check PVC binding:
+```bash
+kubectl get pvc -n monitoring tempo-data-pvc
+```
+
+### No traces appearing
+
+1. Verify Alloy is forwarding traces:
+```bash
+kubectl logs -n monitoring -l app.kubernetes.io/name=alloy | grep -i tempo
+```
+
+2. Check Tempo logs:
+```bash
+just logs
+```
+
+3. Verify OTLP receivers are listening:
+```bash
+just check
+```
+
+### Grafana datasource not appearing
+
+1. Check ConfigMap exists:
+```bash
+kubectl get cm -n monitoring tempo-grafana-datasource --show-labels
+```
+
+2. Check Grafana sidecar logs:
+```bash
+kubectl logs -n monitoring $(kubectl get pod -n monitoring -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].metadata.name}') -c grafana-sc-datasources
+```
+
+3. Restart Grafana pod if needed:
+```bash
+kubectl delete pod -n monitoring -l app.kubernetes.io/name=grafana
+```
+
+## References
+
+- [Grafana Tempo Documentation](https://grafana.com/docs/tempo/latest/)
+- [Tempo Helm Chart](https://github.com/grafana/helm-charts/tree/main/charts/tempo)
+- [OpenTelemetry Protocol (OTLP)](https://opentelemetry.io/docs/specs/otlp/)
+- [TraceQL Query Language](https://grafana.com/docs/tempo/latest/traceql/)
diff --git a/f3s/tempo/datasource-configmap.yaml b/f3s/tempo/datasource-configmap.yaml
new file mode 100644
index 0000000..00fb972
--- /dev/null
+++ b/f3s/tempo/datasource-configmap.yaml
@@ -0,0 +1,47 @@
+# Grafana Datasource ConfigMap for Tempo
+# Auto-discovered by Grafana sidecar via label grafana_datasource: "1"
+# Enables traces-to-logs and traces-to-metrics correlation
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: tempo-grafana-datasource
+ namespace: monitoring
+ labels:
+ grafana_datasource: "1" # Must be string "1" for auto-discovery
+data:
+ tempo-datasource.yaml: |-
+ apiVersion: 1
+ datasources:
+ - name: "Tempo"
+ type: tempo
+ uid: tempo
+ url: http://tempo.monitoring.svc.cluster.local:3200
+ access: proxy
+ isDefault: false
+ editable: true
+ jsonData:
+ httpMethod: GET
+ # Enable traces-to-logs correlation with Loki
+ tracesToLogsV2:
+ datasourceUid: 'loki'
+ spanStartTimeShift: '-1h'
+ spanEndTimeShift: '1h'
+ filterByTraceID: false
+ filterBySpanID: false
+ tags: ['cluster', 'namespace', 'pod', 'app']
+ # Enable traces-to-metrics correlation with Prometheus
+ tracesToMetrics:
+ datasourceUid: 'prometheus'
+ # Enable service graph visualization
+ serviceMap:
+ datasourceUid: 'prometheus'
+ # Enable node graph for visualization
+ nodeGraph:
+ enabled: true
+ # Enable search
+ search:
+ hide: false
+ # Enable Loki search integration
+ lokiSearch:
+ datasourceUid: 'loki'
diff --git a/f3s/tempo/persistent-volumes.yaml b/f3s/tempo/persistent-volumes.yaml
new file mode 100644
index 0000000..fc4c378
--- /dev/null
+++ b/f3s/tempo/persistent-volumes.yaml
@@ -0,0 +1,31 @@
+# Persistent Volume and Claim for Grafana Tempo trace storage
+# Following the pattern from Loki deployment
+# Storage: 10Gi at /data/nfs/k3svolumes/tempo/data
+
+apiVersion: v1
+kind: PersistentVolume
+metadata:
+ name: tempo-data-pv
+spec:
+ capacity:
+ storage: 10Gi
+ volumeMode: Filesystem
+ accessModes:
+ - ReadWriteOnce
+ persistentVolumeReclaimPolicy: Retain
+ hostPath:
+ path: /data/nfs/k3svolumes/tempo/data
+ type: DirectoryOrCreate
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: tempo-data-pvc
+ namespace: monitoring
+spec:
+ storageClassName: "" # Empty for manual binding to PV
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
diff --git a/f3s/tempo/values.yaml b/f3s/tempo/values.yaml
new file mode 100644
index 0000000..d118b63
--- /dev/null
+++ b/f3s/tempo/values.yaml
@@ -0,0 +1,76 @@
+# Grafana Tempo - Monolithic Mode Configuration
+# Following the pattern from Loki SingleBinary deployment
+
+tempo:
+ # Retention policy for traces (7 days)
+ retention: 168h
+
+ # Storage configuration - Local filesystem backend
+ # This is required for monolithic mode
+ storage:
+ trace:
+ backend: local
+ local:
+ path: /var/tempo/traces
+ wal:
+ path: /var/tempo/wal
+
+ # Distributor configuration with OTLP receivers
+ # Bind to 0.0.0.0 to avoid Tempo 2.7+ localhost-only binding issue
+ receivers:
+ otlp:
+ protocols:
+ grpc:
+ endpoint: 0.0.0.0:4317
+ http:
+ endpoint: 0.0.0.0:4318
+
+ # Query frontend configuration
+ # Enabled by default in monolithic mode
+
+# Persistence configuration using hostPath PV
+# Matches the pattern from Loki deployment
+persistence:
+ enabled: true
+ size: 10Gi
+ storageClassName: "" # Empty string for manual PV binding
+ accessModes:
+ - ReadWriteOnce
+
+# Service configuration
+# Expose OTLP ports and query endpoint
+service:
+ type: ClusterIP
+
+# Resource limits to prevent runaway resource usage
+# Adjusted for monolithic deployment
+resources:
+ limits:
+ cpu: 1000m
+ memory: 2Gi
+ requests:
+ cpu: 500m
+ memory: 1Gi
+
+# Security context following best practices
+# Using non-root user
+securityContext:
+ fsGroup: 10001
+ runAsUser: 10001
+ runAsGroup: 10001
+ runAsNonRoot: true
+
+# Disable components not needed in monolithic mode
+gateway:
+ enabled: false
+
+# Monitoring integration with Prometheus
+# Enables ServiceMonitor for automatic scraping
+serviceMonitor:
+ enabled: true
+ labels:
+ release: prometheus
+
+# Test pod disabled to reduce overhead
+test:
+ enabled: false
diff --git a/f3s/tracing-demo/Justfile b/f3s/tracing-demo/Justfile
new file mode 100644
index 0000000..d1bc474
--- /dev/null
+++ b/f3s/tracing-demo/Justfile
@@ -0,0 +1,93 @@
+# Tracing Demo Application deployment automation
+# Three-tier Python application demonstrating distributed tracing
+
+NAMESPACE := "services"
+RELEASE_NAME := "tracing-demo"
+CHART_PATH := "./helm-chart"
+
+# Build all Docker images (use docker-image-Justfile for build/push to registry)
+build:
+ just -f docker-image-Justfile build
+
+# Push images to private registry
+push:
+ just -f docker-image-Justfile push
+
+# Build and push images
+build-push: build push
+
+# Install Helm chart
+install:
+ helm install {{RELEASE_NAME}} {{CHART_PATH}} --namespace {{NAMESPACE}} --create-namespace
+
+# Upgrade Helm chart
+upgrade:
+ helm upgrade {{RELEASE_NAME}} {{CHART_PATH}} --namespace {{NAMESPACE}}
+
+# Delete Helm release
+delete:
+ helm uninstall {{RELEASE_NAME}} --namespace {{NAMESPACE}}
+
+# Rebuild images, import, and upgrade deployment
+rebuild: build import upgrade
+
+# Check deployment status
+status:
+ kubectl get pods -n {{NAMESPACE}} | grep tracing-demo
+ kubectl get svc -n {{NAMESPACE}} | grep -E '(frontend|middleware|backend)-service'
+ kubectl get ingress -n {{NAMESPACE}} tracing-demo-ingress
+
+# View logs from all services
+logs:
+ @echo "=== Frontend logs ==="
+ kubectl logs -n {{NAMESPACE}} -l app=tracing-demo-frontend --tail=20
+ @echo ""
+ @echo "=== Middleware logs ==="
+ kubectl logs -n {{NAMESPACE}} -l app=tracing-demo-middleware --tail=20
+ @echo ""
+ @echo "=== Backend logs ==="
+ kubectl logs -n {{NAMESPACE}} -l app=tracing-demo-backend --tail=20
+
+# Follow logs from frontend
+logs-frontend:
+ kubectl logs -n {{NAMESPACE}} -l app=tracing-demo-frontend -f
+
+# Follow logs from middleware
+logs-middleware:
+ kubectl logs -n {{NAMESPACE}} -l app=tracing-demo-middleware -f
+
+# Follow logs from backend
+logs-backend:
+ kubectl logs -n {{NAMESPACE}} -l app=tracing-demo-backend -f
+
+# Test the application
+test:
+ @echo "Testing frontend health endpoint..."
+ curl http://tracing-demo.f3s.buetow.org/
+ @echo ""
+ @echo "Testing API process endpoint..."
+ curl http://tracing-demo.f3s.buetow.org/api/process
+
+# Load test - generate multiple traces
+load-test:
+ @echo "Generating 50 requests with 0.5s delay..."
+ @for i in {1..50}; do \
+ curl -s http://tracing-demo.f3s.buetow.org/api/process >/dev/null && echo "Request $$i complete"; \
+ sleep 0.5; \
+ done
+ @echo "Load test complete!"
+
+# Port forward to services for local testing
+port-forward-frontend:
+ kubectl port-forward -n {{NAMESPACE}} svc/frontend-service 5000:5000
+
+port-forward-middleware:
+ kubectl port-forward -n {{NAMESPACE}} svc/middleware-service 5001:5001
+
+port-forward-backend:
+ kubectl port-forward -n {{NAMESPACE}} svc/backend-service 5002:5002
+
+# Check if traces are being generated
+check-traces:
+ @echo "Check Grafana Tempo for traces with:"
+ @echo " { resource.service.namespace = \"tracing-demo\" }"
diff --git a/f3s/tracing-demo/README.md b/f3s/tracing-demo/README.md
new file mode 100644
index 0000000..5934c00
--- /dev/null
+++ b/f3s/tracing-demo/README.md
@@ -0,0 +1,250 @@
+# Tracing Demo Application
+
+Three-tier Python Flask application demonstrating distributed tracing with OpenTelemetry and Grafana Tempo.
+
+## Overview
+
+This demo application shows how distributed tracing works across multiple microservices:
+
+- **Frontend**: Receives HTTP requests, forwards to middleware
+- **Middleware**: Transforms data, calls backend
+- **Backend**: Returns data (simulates database queries)
+
+Each service is instrumented with OpenTelemetry and sends traces to Grafana Tempo via Alloy.
+
+## Architecture
+
+```
+User → Frontend (Flask:5000) → Middleware (Flask:5001) → Backend (Flask:5002)
+ ↓ ↓ ↓
+ Alloy (OTLP:4317) → Tempo → Grafana
+```
+
+## Components
+
+### Frontend Service
+- Port: 5000
+- Endpoints:
+ - `GET /` - Service info and health
+ - `GET /health` - Kubernetes health probe
+ - `GET|POST /api/process` - Main processing endpoint
+- Calls: Middleware service
+
+### Middleware Service
+- Port: 5001
+- Endpoints:
+ - `GET /` - Service info and health
+ - `GET /health` - Kubernetes health probe
+ - `POST /api/transform` - Data transformation endpoint
+- Calls: Backend service
+
+### Backend Service
+- Port: 5002
+- Endpoints:
+ - `GET /` - Service info and health
+ - `GET /health` - Kubernetes health probe
+ - `GET /api/data` - Data retrieval endpoint (simulates DB query)
+- Calls: None (leaf service)
+
+## OpenTelemetry Instrumentation
+
+All services use:
+- **Auto-instrumentation**: Flask and Requests libraries automatically create spans
+- **Manual spans**: Custom spans for business logic with attributes
+- **OTLP export**: Traces sent to Alloy via gRPC on port 4317
+- **Resource attributes**: Service name, namespace, version identify each service
+
+## Build and Deploy
+
+### Prerequisites
+
+1. Tempo must be deployed and running in `monitoring` namespace
+2. Alloy must be configured with OTLP receivers
+3. Docker installed for building images
+4. Access to k3s cluster (SSH to r0)
+
+### Quick Start
+
+```bash
+# Build Docker images
+just build
+
+# Import images to k3s
+just import
+
+# Deploy with Helm
+just install
+
+# Check status
+just status
+```
+
+### Rebuild and Update
+
+```bash
+# Rebuild images, import, and upgrade deployment
+just rebuild
+```
+
+## Testing
+
+### Basic Test
+
+```bash
+# Test health endpoint
+curl http://tracing-demo.f3s.buetow.org/
+
+# Test API endpoint (generates a trace)
+curl http://tracing-demo.f3s.buetow.org/api/process
+```
+
+### Load Test
+
+Generate 50 requests to create multiple traces:
+
+```bash
+just load-test
+```
+
+### View Logs
+
+```bash
+# View logs from all services
+just logs
+
+# Follow frontend logs
+just logs-frontend
+
+# Follow middleware logs
+just logs-middleware
+
+# Follow backend logs
+just logs-backend
+```
+
+## Viewing Traces in Grafana
+
+1. Navigate to Grafana: https://grafana.f3s.buetow.org
+2. Go to Explore → Select "Tempo" datasource
+3. Use TraceQL queries:
+
+```
+# All traces from demo app
+{ resource.service.namespace = "tracing-demo" }
+
+# Slow requests (>200ms)
+{ duration > 200ms }
+
+# Traces from specific service
+{ resource.service.name = "frontend" }
+
+# Errors
+{ status = error }
+```
+
+4. View Service Graph to see connections between services
+
+## Trace Features Demonstrated
+
+### Distributed Context Propagation
+Traces automatically span all three services, showing:
+- Frontend span (root)
+- Middleware span (child of frontend)
+- Backend span (child of middleware)
+
+### Custom Attributes
+Each service adds custom attributes:
+- `service.name` - Service identifier
+- `service.namespace` - Application namespace
+- Custom business logic attributes
+
+### Trace Correlation
+- **Traces-to-Logs**: Click on a span to see related logs in Loki
+- **Traces-to-Metrics**: View Prometheus metrics for services in the trace
+- **Service Graph**: Visualize service dependencies
+
+## Development
+
+### Local Testing with Port Forwarding
+
+```bash
+# Forward frontend
+just port-forward-frontend
+curl http://localhost:5000/
+
+# Forward middleware
+just port-forward-middleware
+curl http://localhost:5001/
+
+# Forward backend
+just port-forward-backend
+curl http://localhost:5002/
+```
+
+### Modifying the Application
+
+1. Edit Python code in `docker/*/app.py`
+2. Rebuild: `just build`
+3. Import: `just import`
+4. Upgrade: `just upgrade`
+
+Or use the combined command: `just rebuild`
+
+## Troubleshooting
+
+### No traces appearing in Grafana
+
+1. Check pods are running:
+```bash
+kubectl get pods -n services | grep tracing-demo
+```
+
+2. Check Alloy is receiving traces:
+```bash
+kubectl logs -n monitoring -l app.kubernetes.io/name=alloy | grep -i otlp
+```
+
+3. Check Tempo is storing traces:
+```bash
+kubectl logs -n monitoring -l app.kubernetes.io/name=tempo | grep -i trace
+```
+
+4. Verify OTLP endpoint is accessible:
+```bash
+kubectl exec -n services $(kubectl get pod -n services -l app=tracing-demo-frontend -o jsonpath='{.items[0].metadata.name}') -- wget -qO- http://alloy.monitoring.svc.cluster.local:4317
+```
+
+### Pods not starting
+
+Check events and logs:
+```bash
+kubectl describe pod -n services -l app=tracing-demo-frontend
+kubectl logs -n services -l app=tracing-demo-frontend
+```
+
+### Images not found
+
+Verify images are imported to k3s:
+```bash
+ssh r0 'k3s crictl images | grep tracing-demo'
+```
+
+If missing, run:
+```bash
+just import
+```
+
+## Cleanup
+
+Remove the demo application:
+
+```bash
+just delete
+```
+
+## References
+
+- [OpenTelemetry Python Documentation](https://opentelemetry.io/docs/languages/python/)
+- [Flask Instrumentation](https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/flask/flask.html)
+- [Grafana Tempo Documentation](https://grafana.com/docs/tempo/latest/)
+- [TraceQL Query Language](https://grafana.com/docs/tempo/latest/traceql/)
diff --git a/f3s/tracing-demo/docker-image-Justfile b/f3s/tracing-demo/docker-image-Justfile
new file mode 100644
index 0000000..7b263b1
--- /dev/null
+++ b/f3s/tracing-demo/docker-image-Justfile
@@ -0,0 +1,38 @@
+# Docker image build and push automation for tracing demo
+# Similar to anki-sync-server pattern
+
+REGISTRY := "r0.lan.buetow.org:30001"
+
+# Build all images
+build:
+ docker build -t tracing-demo-frontend:latest ./docker/frontend
+ docker build -t tracing-demo-middleware:latest ./docker/middleware
+ docker build -t tracing-demo-backend:latest ./docker/backend
+
+# Tag and push all images to private registry
+push:
+ docker tag tracing-demo-frontend:latest {{REGISTRY}}/tracing-demo-frontend:latest
+ docker tag tracing-demo-middleware:latest {{REGISTRY}}/tracing-demo-middleware:latest
+ docker tag tracing-demo-backend:latest {{REGISTRY}}/tracing-demo-backend:latest
+ docker push {{REGISTRY}}/tracing-demo-frontend:latest
+ docker push {{REGISTRY}}/tracing-demo-middleware:latest
+ docker push {{REGISTRY}}/tracing-demo-backend:latest
+
+# Build and push in one command
+all: build push
+
+# Build and push specific service
+frontend:
+ docker build -t tracing-demo-frontend:latest ./docker/frontend
+ docker tag tracing-demo-frontend:latest {{REGISTRY}}/tracing-demo-frontend:latest
+ docker push {{REGISTRY}}/tracing-demo-frontend:latest
+
+middleware:
+ docker build -t tracing-demo-middleware:latest ./docker/middleware
+ docker tag tracing-demo-middleware:latest {{REGISTRY}}/tracing-demo-middleware:latest
+ docker push {{REGISTRY}}/tracing-demo-middleware:latest
+
+backend:
+ docker build -t tracing-demo-backend:latest ./docker/backend
+ docker tag tracing-demo-backend:latest {{REGISTRY}}/tracing-demo-backend:latest
+ docker push {{REGISTRY}}/tracing-demo-backend:latest
diff --git a/f3s/tracing-demo/docker/backend/Dockerfile b/f3s/tracing-demo/docker/backend/Dockerfile
new file mode 100644
index 0000000..5018e8f
--- /dev/null
+++ b/f3s/tracing-demo/docker/backend/Dockerfile
@@ -0,0 +1,16 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Copy and install dependencies
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy application code
+COPY app.py .
+
+# Expose port for Flask application
+EXPOSE 5002
+
+# Run the application
+CMD ["python", "app.py"]
diff --git a/f3s/tracing-demo/docker/backend/app.py b/f3s/tracing-demo/docker/backend/app.py
new file mode 100644
index 0000000..2c9e88a
--- /dev/null
+++ b/f3s/tracing-demo/docker/backend/app.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+"""
+Tracing Demo - Backend Service
+Final service in the chain that returns data.
+Simulates database queries and demonstrates end-to-end tracing.
+"""
+from flask import Flask, jsonify
+import os
+import logging
+import time
+from datetime import datetime
+
+# OpenTelemetry imports for distributed tracing
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import BatchSpanProcessor
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+from opentelemetry.instrumentation.flask import FlaskInstrumentor
+from opentelemetry.sdk.resources import Resource
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+# Initialize OpenTelemetry tracing with resource attributes
+# These attributes identify this service in traces
+resource = Resource(attributes={
+ "service.name": "backend",
+ "service.namespace": "tracing-demo",
+ "service.version": "1.0.0",
+ "deployment.environment": "production"
+})
+
+provider = TracerProvider(resource=resource)
+
+# Configure OTLP exporter to send traces to Alloy
+otlp_exporter = OTLPSpanExporter(
+ endpoint=os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT',
+ 'http://alloy.monitoring.svc.cluster.local:4317'),
+ insecure=True
+)
+
+# Batch spans for efficient export
+processor = BatchSpanProcessor(otlp_exporter)
+provider.add_span_processor(processor)
+trace.set_tracer_provider(provider)
+
+# Get tracer for manual instrumentation
+tracer = trace.get_tracer(__name__)
+