SSO, Tracing, Alerting & Long-Term Storage
Enterprise-grade observability enhancements
Add SSO, distributed tracing, advanced alerting, and unlimited retention!
| Component | Time | Difficulty |
|---|---|---|
| Authentik SSO for Grafana | 30-45 min | Medium |
| Additional Exporters | 30-45 min | Easy |
| Project Dashboards | 45-60 min | Medium |
| Alertmanager Setup | 30-45 min | Medium |
| Tempo Tracing | 45-60 min | Advanced |
| Thanos Long-Term Storage | 60-90 min | Advanced |
Total: 5-7 hours
Login to Grafana with your Authentik account
One login for everything - Grafana, Vault, Teleport, and more!
| Field | Value |
|---|---|
| Name | Grafana OIDC Provider |
| Authorization flow | default-provider-authorization-implicit-consent |
| Client type | Confidential |
| Client ID | grafana |
| Redirect URIs | https://grafana.yourdomain.com/login/generic_oauth |
| Name | Grafana |
| Slug | grafana |
| Provider | Grafana OIDC Provider |
| Launch URL | https://grafana.yourdomain.com |
# Update Grafana service in docker-compose.yml
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
# Basic settings
- GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
- GF_SERVER_DOMAIN=grafana.yourdomain.com
# Disable default login (optional)
- GF_AUTH_DISABLE_LOGIN_FORM=false
- GF_AUTH_DISABLE_SIGNOUT_MENU=false
# OAuth2 / OIDC Configuration
- GF_AUTH_GENERIC_OAUTH_ENABLED=true
- GF_AUTH_GENERIC_OAUTH_NAME=Authentik
- GF_AUTH_GENERIC_OAUTH_CLIENT_ID=grafana
- GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=YOUR_CLIENT_SECRET_HERE
- GF_AUTH_GENERIC_OAUTH_SCOPES=openid profile email
- GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://authentik.yourdomain.com/application/o/authorize/
- GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://authentik.yourdomain.com/application/o/token/
- GF_AUTH_GENERIC_OAUTH_API_URL=https://authentik.yourdomain.com/application/o/userinfo/
- GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(groups[*], 'Grafana Admins') && 'Admin' || contains(groups[*], 'Grafana Editors') && 'Editor' || 'Viewer'
- GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true
# Sign out redirect
- GF_AUTH_SIGNOUT_REDIRECT_URL=https://authentik.yourdomain.com/application/o/grafana/end-session/
volumes:
- ./grafana/data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- monitoring
Replace yourdomain.com with your actual domain and YOUR_CLIENT_SECRET_HERE with the secret from Authentik.
Create groups in Authentik for role mapping:
| Group Name | Grafana Role |
|---|---|
| Grafana Admins | Admin (full access) |
| Grafana Editors | Editor (create dashboards) |
| Grafana Viewers | Viewer (read-only) |
cd ~/monitoring
# Restart Grafana
docker compose up -d grafana
# Check logs
docker logs grafana --tail=50
Open Grafana and click "Sign in with Authentik". You'll be redirected to Authentik for login!
Collect metrics from all your services
Monitor Authentik, Vault, PostgreSQL, Traefik, and more!
Monitor your PostgreSQL databases (Authentik, etc.):
# Add to docker-compose.yml
postgres-exporter:
image: prometheuscommunity/postgres-exporter:latest
container_name: postgres-exporter
restart: unless-stopped
environment:
- DATA_SOURCE_NAME=postgresql://username:password@postgres:5432/authentik?sslmode=disable
ports:
- "9187:9187"
networks:
- monitoring
# Add to prometheus.yml scrape_configs:
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
Monitor endpoint availability and response times:
# Create blackbox.yml config
cat > ~/monitoring/blackbox/blackbox.yml << 'EOF'
modules:
http_2xx:
prober: http
timeout: 5s
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
valid_status_codes: [200, 201, 301, 302]
method: GET
follow_redirects: true
preferred_ip_protocol: "ip4"
tcp_connect:
prober: tcp
timeout: 5s
EOF
# Docker Compose service
blackbox-exporter:
image: prom/blackbox-exporter:latest
container_name: blackbox-exporter
restart: unless-stopped
ports:
- "9115:9115"
volumes:
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/config.yml:ro
command:
- '--config.file=/etc/blackbox_exporter/config.yml'
networks:
- monitoring
# Prometheus scrape config for HTTP probes
- job_name: 'blackbox-http'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://authentik.yourdomain.com
- https://vault.yourdomain.com
- https://grafana.yourdomain.com
- https://teleport.yourdomain.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
# Add to traefik.yml or command line:
metrics:
prometheus:
buckets:
- 0.1
- 0.3
- 1.2
- 5.0
addEntryPointsLabels: true
addRoutersLabels: true
addServicesLabels: true
entryPoint: metrics
entryPoints:
metrics:
address: ":8082"
# Prometheus scrape config
- job_name: 'traefik'
static_configs:
- targets: ['traefik:8082']
| Exporter | Monitors | Port |
|---|---|---|
| node-exporter | Linux system (CPU, memory, disk) | 9100 |
| cAdvisor | Docker containers | 8080 |
| postgres-exporter | PostgreSQL databases | 9187 |
| blackbox-exporter | HTTP/TCP endpoints | 9115 |
| traefik | Reverse proxy metrics | 8082 |
Visualize all your projects in Grafana
One dashboard to rule them all - see Authentik, Vault, Teleport, and Wazuh!
Create a master dashboard showing all projects:
# Create this file and import into Grafana
# Dashboards → Import → Upload JSON
{
"title": "Home Lab Overview",
"uid": "homelab-overview",
"tags": ["homelab", "overview"],
"panels": [
{
"title": "Service Status",
"type": "stat",
"gridPos": { "x": 0, "y": 0, "w": 24, "h": 4 },
"targets": [{
"expr": "up{job=~\"blackbox-http\"}",
"legendFormat": "{{instance}}"
}],
"fieldConfig": {
"defaults": {
"mappings": [
{ "type": "value", "options": { "0": { "text": "DOWN", "color": "red" } } },
{ "type": "value", "options": { "1": { "text": "UP", "color": "green" } } }
]
}
}
},
{
"title": "CPU Usage",
"type": "gauge",
"gridPos": { "x": 0, "y": 4, "w": 6, "h": 6 },
"targets": [{
"expr": "100 - (avg(irate(node_cpu_seconds_total{mode=\"idle\"}[5m])) * 100)"
}]
},
{
"title": "Memory Usage",
"type": "gauge",
"gridPos": { "x": 6, "y": 4, "w": 6, "h": 6 },
"targets": [{
"expr": "(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100"
}]
},
{
"title": "Disk Usage",
"type": "gauge",
"gridPos": { "x": 12, "y": 4, "w": 6, "h": 6 },
"targets": [{
"expr": "(1 - (node_filesystem_avail_bytes{mountpoint=\"/\"} / node_filesystem_size_bytes{mountpoint=\"/\"})) * 100"
}]
},
{
"title": "Container Count",
"type": "stat",
"gridPos": { "x": 18, "y": 4, "w": 6, "h": 6 },
"targets": [{
"expr": "count(container_last_seen{name!=\"\"})"
}]
}
]
}
Authentik exposes Prometheus metrics natively:
# Add to Authentik docker-compose environment:
AUTHENTIK_METRICS__ENABLED=true
# Prometheus scrape config
- job_name: 'authentik'
static_configs:
- targets: ['authentik-server:9300']
# Key metrics to monitor:
# - authentik_flows_execution_count
# - authentik_outpost_connection_count
# - authentik_policies_execution_time
# - authentik_stages_execution_time
# Add to vault config (vault-config.hcl):
telemetry {
prometheus_retention_time = "60s"
disable_hostname = true
}
# Prometheus scrape config
- job_name: 'vault'
metrics_path: '/v1/sys/metrics'
params:
format: ['prometheus']
scheme: https
tls_config:
insecure_skip_verify: true
bearer_token: YOUR_VAULT_TOKEN
static_configs:
- targets: ['vault:8200']
# Key metrics:
# - vault_core_unsealed (1 = unsealed)
# - vault_token_count
# - vault_secret_lease_count
# - vault_audit_log_request_count
Send Wazuh alerts to Loki for correlation:
# Add Wazuh alerts to Promtail config:
scrape_configs:
- job_name: wazuh-alerts
static_configs:
- targets:
- localhost
labels:
job: wazuh
__path__: /var/ossec/logs/alerts/alerts.json
pipeline_stages:
- json:
expressions:
level: rule.level
description: rule.description
agent: agent.name
- labels:
level:
agent:
# Sample LogQL queries for Wazuh:
# High severity alerts
{job="wazuh"} | json | level >= 10
# Authentication failures
{job="wazuh"} |= "authentication" |= "failed"
Advanced alert routing and management
Route critical alerts to Slack, warnings to email, group by service!
# Add to docker-compose.yml
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
restart: unless-stopped
ports:
- "9093:9093"
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- ./alertmanager/data:/alertmanager
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
- '--storage.path=/alertmanager'
networks:
- monitoring
cat > ~/monitoring/alertmanager/alertmanager.yml << 'EOF'
global:
resolve_timeout: 5m
slack_api_url: 'YOUR_SLACK_WEBHOOK_URL'
route:
receiver: 'default-receiver'
group_by: ['alertname', 'severity', 'service']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# Critical alerts -> Slack immediately
- match:
severity: critical
receiver: 'slack-critical'
group_wait: 10s
repeat_interval: 1h
# High alerts -> Slack
- match:
severity: high
receiver: 'slack-high'
# Warning alerts -> Email
- match:
severity: warning
receiver: 'email-warnings'
# Security alerts -> Dedicated channel
- match:
service: wazuh
receiver: 'slack-security'
receivers:
- name: 'default-receiver'
slack_configs:
- channel: '#alerts'
send_resolved: true
title: '{{ .Status | toUpper }}: {{ .CommonAnnotations.summary }}'
text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'
- name: 'slack-critical'
slack_configs:
- channel: '#alerts-critical'
send_resolved: true
color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'
title: '🚨 CRITICAL: {{ .CommonAnnotations.summary }}'
text: |
*Alert:* {{ .CommonLabels.alertname }}
*Description:* {{ .CommonAnnotations.description }}
*Severity:* {{ .CommonLabels.severity }}
{{ range .Alerts }}
*Details:*
{{ range .Labels.SortedPairs }} - {{ .Name }}: {{ .Value }}
{{ end }}
{{ end }}
- name: 'slack-high'
slack_configs:
- channel: '#alerts'
send_resolved: true
color: 'warning'
title: '⚠️ HIGH: {{ .CommonAnnotations.summary }}'
- name: 'slack-security'
slack_configs:
- channel: '#security-alerts'
send_resolved: true
color: 'danger'
title: '🛡️ Security: {{ .CommonAnnotations.summary }}'
- name: 'email-warnings'
email_configs:
- to: 'alerts@yourdomain.com'
from: 'alertmanager@yourdomain.com'
smarthost: 'smtp.gmail.com:587'
auth_username: 'your-email@gmail.com'
auth_password: 'your-app-password'
send_resolved: true
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'service']
EOF
cat > ~/monitoring/prometheus/alert-rules.yml << 'EOF'
groups:
- name: infrastructure
rules:
# High CPU
- alert: HighCPU
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
service: infrastructure
annotations:
summary: "High CPU usage on {{ $labels.instance }}"
description: "CPU usage is {{ $value | printf \"%.1f\" }}%"
# Critical CPU
- alert: CriticalCPU
expr: 100 - (avg by(instance) (irate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 95
for: 2m
labels:
severity: critical
service: infrastructure
annotations:
summary: "Critical CPU on {{ $labels.instance }}"
description: "CPU at {{ $value | printf \"%.1f\" }}% - immediate attention needed"
# High Memory
- alert: HighMemory
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: warning
service: infrastructure
annotations:
summary: "High memory usage on {{ $labels.instance }}"
description: "Memory usage is {{ $value | printf \"%.1f\" }}%"
# Disk Almost Full
- alert: DiskAlmostFull
expr: (1 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"})) * 100 > 85
for: 5m
labels:
severity: critical
service: infrastructure
annotations:
summary: "Disk almost full on {{ $labels.instance }}"
description: "Disk usage is {{ $value | printf \"%.1f\" }}%"
- name: services
rules:
# Service Down
- alert: ServiceDown
expr: up{job=~"blackbox-http"} == 0
for: 1m
labels:
severity: critical
service: "{{ $labels.instance }}"
annotations:
summary: "Service {{ $labels.instance }} is DOWN"
description: "The service has been unreachable for more than 1 minute"
# High Response Time
- alert: HighResponseTime
expr: probe_http_duration_seconds{phase="transfer"} > 2
for: 5m
labels:
severity: warning
service: "{{ $labels.instance }}"
annotations:
summary: "Slow response from {{ $labels.instance }}"
description: "Response time is {{ $value | printf \"%.2f\" }}s"
- name: containers
rules:
# Container Down
- alert: ContainerDown
expr: absent(container_last_seen{name=~".+"})
for: 1m
labels:
severity: critical
service: docker
annotations:
summary: "Container {{ $labels.name }} is down"
description: "Container has stopped running"
# High Container Memory
- alert: ContainerHighMemory
expr: (container_memory_usage_bytes{name!=""} / container_spec_memory_limit_bytes{name!=""}) * 100 > 80
for: 5m
labels:
severity: warning
service: docker
annotations:
summary: "Container {{ $labels.name }} high memory"
description: "Memory usage is {{ $value | printf \"%.1f\" }}%"
EOF
# Update prometheus.yml to include rules:
rule_files:
- /etc/prometheus/alert-rules.yml
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
Track requests across services
See exactly where time is spent as requests flow through your stack!
# Create tempo config
mkdir -p ~/monitoring/tempo
cat > ~/monitoring/tempo/tempo.yml << 'EOF'
server:
http_listen_port: 3200
distributor:
receivers:
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
grpc:
endpoint: 0.0.0.0:14250
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
ingester:
trace_idle_period: 10s
max_block_bytes: 1_000_000
max_block_duration: 5m
compactor:
compaction:
compaction_window: 1h
max_block_bytes: 100_000_000
block_retention: 48h
compacted_block_retention: 1h
storage:
trace:
backend: local
local:
path: /tmp/tempo/blocks
wal:
path: /tmp/tempo/wal
EOF
# Docker Compose service
tempo:
image: grafana/tempo:latest
container_name: tempo
restart: unless-stopped
command: ["-config.file=/etc/tempo/tempo.yml"]
ports:
- "3200:3200" # Tempo API
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "14268:14268" # Jaeger HTTP
volumes:
- ./tempo/tempo.yml:/etc/tempo/tempo.yml:ro
- ./tempo/data:/tmp/tempo
networks:
- monitoring
# Add to datasources.yml
- name: Tempo
type: tempo
access: proxy
url: http://tempo:3200
editable: false
jsonData:
httpMethod: GET
tracesToLogs:
datasourceUid: loki
tags: ['job', 'instance']
mappedTags: [{ key: 'service.name', value: 'service' }]
mapTagNamesEnabled: true
filterByTraceID: true
tracesToMetrics:
datasourceUid: prometheus
tags: [{ key: 'service.name', value: 'service' }]
serviceMap:
datasourceUid: prometheus
nodeGraph:
enabled: true
Your applications need to send traces to Tempo. Use OpenTelemetry SDKs or Jaeger clients to instrument your code.
Unlimited metric retention with object storage
Keep years of metrics in cheap object storage (S3/MinIO)
# Add MinIO to docker-compose.yml
minio:
image: minio/minio:latest
container_name: minio
restart: unless-stopped
ports:
- "9000:9000"
- "9001:9001"
environment:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=YOUR_SECURE_PASSWORD
command: server /data --console-address ":9001"
volumes:
- ./minio/data:/data
networks:
- monitoring
# Create bucket for Thanos
# Access MinIO console at http://SERVER_IP:9001
# Create bucket named "thanos"
# Create bucket config
cat > ~/monitoring/thanos/bucket.yml << 'EOF'
type: S3
config:
bucket: thanos
endpoint: minio:9000
access_key: minioadmin
secret_key: YOUR_SECURE_PASSWORD
insecure: true
EOF
# Update Prometheus to allow Thanos access
# Add to prometheus command:
prometheus:
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=2h' # Short retention, Thanos handles long-term
- '--storage.tsdb.min-block-duration=2h'
- '--storage.tsdb.max-block-duration=2h'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
# Add Thanos Sidecar
thanos-sidecar:
image: quay.io/thanos/thanos:latest
container_name: thanos-sidecar
restart: unless-stopped
command:
- sidecar
- --tsdb.path=/prometheus
- --prometheus.url=http://prometheus:9090
- --objstore.config-file=/etc/thanos/bucket.yml
- --grpc-address=0.0.0.0:10901
- --http-address=0.0.0.0:10902
volumes:
- ./prometheus/data:/prometheus:ro
- ./thanos/bucket.yml:/etc/thanos/bucket.yml:ro
depends_on:
- prometheus
- minio
networks:
- monitoring
# Add to docker-compose.yml
thanos-store:
image: quay.io/thanos/thanos:latest
container_name: thanos-store
restart: unless-stopped
command:
- store
- --data-dir=/tmp/thanos/store
- --objstore.config-file=/etc/thanos/bucket.yml
- --grpc-address=0.0.0.0:10901
- --http-address=0.0.0.0:10902
volumes:
- ./thanos/bucket.yml:/etc/thanos/bucket.yml:ro
- ./thanos/store:/tmp/thanos/store
depends_on:
- minio
networks:
- monitoring
thanos-query:
image: quay.io/thanos/thanos:latest
container_name: thanos-query
restart: unless-stopped
ports:
- "10902:10902"
command:
- query
- --http-address=0.0.0.0:10902
- --grpc-address=0.0.0.0:10901
- --store=thanos-sidecar:10901
- --store=thanos-store:10901
depends_on:
- thanos-sidecar
- thanos-store
networks:
- monitoring
thanos-compactor:
image: quay.io/thanos/thanos:latest
container_name: thanos-compactor
restart: unless-stopped
command:
- compact
- --data-dir=/tmp/thanos/compact
- --objstore.config-file=/etc/thanos/bucket.yml
- --wait
volumes:
- ./thanos/bucket.yml:/etc/thanos/bucket.yml:ro
- ./thanos/compact:/tmp/thanos/compact
depends_on:
- minio
networks:
- monitoring
# Update datasources.yml - change Prometheus URL to Thanos
- name: Prometheus
type: prometheus
access: proxy
url: http://thanos-query:10902 # Changed from prometheus:9090
isDefault: true
editable: false
jsonData:
httpMethod: POST
manageAlerts: true
prometheusType: Thanos
prometheusVersion: 0.31.0
Metrics are now stored in MinIO/S3. Query years of data seamlessly through Grafana!
Verify all enhancements work
Test every component of your advanced observability stack
# Check all services
docker compose ps
# Test Alertmanager
curl http://localhost:9093/-/healthy
# Test Tempo
curl http://localhost:3200/ready
# Test Thanos Query
curl http://localhost:10902/-/healthy
# Check Thanos stores
curl http://localhost:10902/api/v1/stores
# Test MinIO
curl http://localhost:9000/minio/health/live
# Send test alert
curl -X POST http://localhost:9093/api/v1/alerts \
-H "Content-Type: application/json" \
-d '[{"labels":{"alertname":"TestAlert","severity":"warning"},"annotations":{"summary":"Test alert"}}]'
Enterprise-grade observability achieved!
You've built a production-ready monitoring stack with SSO, tracing, and unlimited retention!
| Project | Component | Skills |
|---|---|---|
| A | Authentik SSO | Identity, MFA, OIDC/SAML |
| B | HashiCorp Vault | Secrets, JIT Access, Dynamic Creds |
| C | Teleport | ZTNA, SSH Certificates, RBAC |
| D | Wazuh SIEM | Security Monitoring, XDR, Compliance |
| E | Grafana Stack | Metrics, Logs, Dashboards |
| F | Advanced Observability | SSO, Tracing, Long-Term Storage |