Modern web altyapılarında sadece sunucunun “çalışıyor” olması yeterli değil. Neyin ne kadar çalıştığını, hangi endpoint’lerin yük altında olduğunu, hata oranlarının ne zaman yükseldiğini gerçek zamanlı görebilmek gerekiyor. Caddy bu konuda oldukça iyi düşünülmüş bir metrik sistemiyle geliyor ve Prometheus ile entegrasyonu beklediğinden çok daha az uğraş istiyor. Bu yazıda Caddy’nin built-in metrik desteğini nasıl aktif edeceğimizi, Prometheus ile nasıl scrape edeceğimizi ve Grafana’da anlamlı dashboardlar oluşturmak için ne yapabileceğimizi adım adım inceleyeceğiz.
Caddy Metrics Nedir, Neden Önemli?
Caddy 2.x sürümüyle birlikte OpenMetrics/Prometheus formatında metrik yayınlama özelliği built-in olarak geldi. Yani ek bir eklenti veya sidecar container’a ihtiyaç duymadan Caddy kendi /metrics endpoint’ini açabiliyor.
Bu endpoint üzerinden şu bilgilere ulaşabiliyoruz:
- caddy_http_requests_total: Toplam HTTP istek sayısı, method ve status code’a göre ayrıştırılmış
- caddy_http_request_duration_seconds: İstek süreleri histogram şeklinde
- caddy_http_response_size_bytes: Response boyutları
- caddy_http_active_requests: Anlık aktif istek sayısı
- caddy_reverse_proxy_upstreams_healthy: Reverse proxy upstream sağlık durumu
- process_cpu_seconds_total: CPU kullanımı
- process_resident_memory_bytes: Bellek kullanımı
- go_goroutines: Go runtime goroutine sayısı
Bunlar küçük bir site için fazla gibi görünebilir ama 5-10 backend servisinizi Caddy üzerinden reverse proxy ile yönetiyorsanız, upstream health durumunu ve per-route latency’yi görmek operasyonel açıdan çok kritik hale geliyor.
Kurulum Ortamını Hazırlama
Bu yazıda Ubuntu 22.04 üzerinde çalışacağız. Caddy’nin zaten kurulu olduğunu varsayarak, Prometheus ve Grafana’yı Docker Compose ile ayağa kaldıracağız. Hem izolasyon hem de taşınabilirlik açısından bu yaklaşım production’a en yakın senaryoyu sunuyor.
Önce Caddy’nin kurulu olup olmadığını kontrol edelim:
caddy version
# caddy v2.7.6 h1:...
systemctl status caddy
Eğer Caddy kurulu değilse resmi repo’dan kurabilirsiniz:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Caddy’de Metrics Endpoint’ini Aktif Etme
Caddy’de metrik desteğini açmak için iki farklı yol var. Birincisi global admin API üzerinden, ikincisi ise ayrı bir HTTP endpoint olarak. Her iki yaklaşımın da kendine göre avantajları var.
Yöntem 1: Admin API Üzerinden (Lokal Erişim)
Caddy varsayılan olarak localhost:2019 üzerinde admin API sunuyor. Bu API /metrics endpoint’i de içeriyor ancak dışarıya açık değil. Sadece localhost’tan erişilebilir olması güvenlik açısından avantajlı.
/etc/caddy/Caddyfile dosyasını açın:
sudo nano /etc/caddy/Caddyfile
Global ayarlar bloğuna şunu ekleyin:
{
admin localhost:2019
servers {
metrics
}
}
example.com {
reverse_proxy localhost:8080
}
Yöntem 2: Ayrı Bir Port Üzerinde Metrics Sunmak
Production ortamlarında genellikle metrics endpoint’ini ayrı bir port üzerinde, sadece monitoring altyapısının erişebileceği şekilde açmak daha sağlıklı. Bunun için Caddyfile’a ayrı bir site bloğu ekliyoruz:
{
admin off
servers {
metrics
}
}
# Ana site
example.com {
reverse_proxy localhost:8080
log {
output file /var/log/caddy/access.log
format json
}
}
# Metrics endpoint - sadece internal ağdan erişilebilir
:9090 {
metrics /metrics
# Sadece belirli IP'lerden erişime izin ver
@allowed remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.1
handle @allowed {
metrics /metrics
}
respond 403
}
Burada dikkat edilmesi gereken nokta: eğer admin off derseniz /metrics endpoint’i sadece Caddyfile içinde tanımladığınız site bloğu üzerinden erişilebilir olur.
Yapılandırmayı test edip yeniden yükleyelim:
caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Metrics endpoint’inin çalıştığını doğrulayalım:
curl -s http://localhost:9090/metrics | head -30
Çıktıda şuna benzer bir şey görmelisiniz:
# HELP caddy_http_requests_total Counter of HTTP(S) requests made.
# TYPE caddy_http_requests_total counter
caddy_http_requests_total{code="200",handler="reverse_proxy",method="GET",server="srv0"} 1547
caddy_http_requests_total{code="404",handler="static",method="GET",server="srv0"} 23
# HELP caddy_http_request_duration_seconds Histogram of round-trip time for HTTP requests.
# TYPE caddy_http_request_duration_seconds histogram
Prometheus Kurulumu ve Caddy’yi Scrape Etme
Şimdi Prometheus’u ayağa kaldıralım. Docker Compose kullanacağız. Bir çalışma dizini oluşturun:
mkdir -p ~/monitoring/{prometheus,grafana/provisioning/{datasources,dashboards}}
cd ~/monitoring
Prometheus yapılandırma dosyasını oluşturun:
cat > prometheus/prometheus.yml << 'EOF'
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
environment: 'production'
region: 'eu-west-1'
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'caddy'
static_configs:
- targets: ['172.17.0.1:9090']
metrics_path: '/metrics'
scrape_interval: 10s
scrape_timeout: 5s
relabel_configs:
- source_labels: [__address__]
target_label: instance
replacement: 'caddy-prod-01'
# Birden fazla Caddy instance varsa
- job_name: 'caddy-multi'
static_configs:
- targets:
- '10.0.1.10:9090'
- '10.0.1.11:9090'
- '10.0.1.12:9090'
labels:
cluster: 'web-tier'
EOF
Burada 172.17.0.1 Docker’ın host makineye erişmek için kullandığı default bridge IP’si. Eğer Prometheus ve Caddy aynı makinedeyse ama biri Docker içinde biri dışındaysa bu IP’yi kullanıyoruz.
Docker Compose dosyasını oluşturun:
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
prometheus:
image: prom/prometheus:v2.48.0
container_name: prometheus
restart: unless-stopped
ports:
- "127.0.0.1:9191:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
- '--web.enable-lifecycle'
networks:
- monitoring
grafana:
image: grafana/grafana:10.2.2
container_name: grafana
restart: unless-stopped
ports:
- "127.0.0.1:3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=supersecret123
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=https://grafana.example.com
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
networks:
- monitoring
volumes:
prometheus_data:
grafana_data:
networks:
monitoring:
driver: bridge
EOF
Stack’i başlatalım:
docker compose up -d
docker compose ps
Prometheus’un Caddy’yi başarıyla scrape edip etmediğini kontrol etmek için:
# Prometheus UI'a Caddy ile erişim sağlayabiliriz
curl -s http://localhost:9191/api/v1/targets | python3 -m json.tool | grep -A5 "caddy"
Caddy’yi Prometheus UI Üzerinden Expose Etme
Prometheus’a ve Grafana’ya production’da Caddy üzerinden erişmek güvenlik açısından doğru olan yaklaşım. Bunun için Caddyfile’ı güncelleyelim:
{
admin off
servers {
metrics
}
email [email protected]
}
example.com {
reverse_proxy localhost:8080
log {
output file /var/log/caddy/access.log
format json
}
}
grafana.example.com {
reverse_proxy localhost:3000
# Basic auth eklemek isterseniz
# basicauth {
# admin $2a$14$...
# }
}
prometheus.example.com {
reverse_proxy localhost:9191
# Prometheus UI'a sadece belirli IP'lerden erişim
@blocked not remote_ip 10.0.0.0/8 192.168.0.0/16
respond @blocked "Access Denied" 403
}
# Internal metrics endpoint
:9090 {
@internal remote_ip 127.0.0.1 10.0.0.0/8 172.16.0.0/12
handle @internal {
metrics /metrics
}
respond 403
}
Prometheus’ta Anlamlı Sorgular Yazmak
Prometheus UI’da şu PromQL sorgularını deneyebilirsiniz:
Saniye başına istek sayısı (son 5 dakika ortalaması):
rate(caddy_http_requests_total[5m])
HTTP 5xx hata oranı:
sum(rate(caddy_http_requests_total{code=~"5.."}[5m])) /
sum(rate(caddy_http_requests_total[5m])) * 100
95. yüzdelik istek süresi:
histogram_quantile(0.95,
sum(rate(caddy_http_request_duration_seconds_bucket[5m])) by (le, server)
)
Aktif upstream sayısı:
caddy_reverse_proxy_upstreams_healthy
Bellek kullanımı (MB cinsinden):
process_resident_memory_bytes{job="caddy"} / 1024 / 1024
Bu sorguları kayıt kuralları olarak tanımlamak, özellikle karmaşık sorgular için performans açısından faydalı. prometheus/rules.yml dosyası oluşturun:
cat > prometheus/rules.yml << 'EOF'
groups:
- name: caddy_recording_rules
interval: 30s
rules:
- record: job:caddy_http_requests:rate5m
expr: sum by (job, server, code) (rate(caddy_http_requests_total[5m]))
- record: job:caddy_http_request_duration_p95:rate5m
expr: histogram_quantile(0.95, sum by (le, server) (rate(caddy_http_request_duration_seconds_bucket[5m])))
- record: job:caddy_error_rate:rate5m
expr: |
sum by (server) (rate(caddy_http_requests_total{code=~"5.."}[5m]))
/
sum by (server) (rate(caddy_http_requests_total[5m]))
- name: caddy_alerts
rules:
- alert: CaddyHighErrorRate
expr: job:caddy_error_rate:rate5m > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "Caddy yüksek hata oranı: {{ $labels.server }}"
description: "Son 5 dakikada hata oranı %{{ printf "%.1f" (mul $value 100) }} seviyesinde."
- alert: CaddyUpstreamDown
expr: caddy_reverse_proxy_upstreams_healthy == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Caddy upstream erişilemiyor"
description: "{{ $labels.upstream }} upstream'i sağlıksız durumda."
- alert: CaddyHighLatency
expr: job:caddy_http_request_duration_p95:rate5m > 2
for: 5m
labels:
severity: warning
annotations:
summary: "Caddy yüksek gecikme süresi"
description: "P95 yanıt süresi 2 saniyeyi aşıyor: {{ $value }}s"
EOF
prometheus.yml dosyasında bu rule dosyasına referans ekleyin:
rule_files:
- '/etc/prometheus/rules.yml'
Ve Prometheus container’ına mount edin:
# docker-compose.yml prometheus service volumes bölümüne ekleyin:
- ./prometheus/rules.yml:/etc/prometheus/rules.yml:ro
Grafana Dashboard Yapılandırması
Grafana’ya ilk girişte datasource tanımlaması yapmanız gerekiyor. Provisioning ile bunu otomatikleştirebilirsiniz:
cat > grafana/provisioning/datasources/prometheus.yml << 'EOF'
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
jsonData:
httpMethod: POST
exemplarTraceIdDestinations:
- name: trace_id
datasourceUid: tempo
EOF
Dashboard provisioning için de bir yapılandırma oluşturun:
cat > grafana/provisioning/dashboards/caddy.yml << 'EOF'
apiVersion: 1
providers:
- name: 'Caddy Dashboards'
orgId: 1
folder: 'Caddy'
type: file
disableDeletion: false
editable: true
options:
path: /etc/grafana/provisioning/dashboards
EOF
Grafana Marketplace’de “Caddy” aratırsanız hazır dashboard’lar bulabilirsiniz. Dashboard ID 20802 özellikle iyi bir başlangıç noktası. Import etmek için Grafana UI’da Dashboards > Import > Dashboard ID alanına bu ID’yi girin.
Gerçek Dünya Senaryosu: Multi-Service Ortamda Monitoring
Diyelim ki 3 farklı servisiniz var: bir API gateway, bir frontend uygulaması ve bir webhook handler. Her birini Caddy’nin farklı virtual host’larında reverse proxy ile yönetiyorsunuz ve her birinin metriklerini ayrı ayrı izlemek istiyorsunuz.
{
admin off
servers {
metrics
}
}
# API Gateway
api.example.com {
@v1 path /v1/*
@v2 path /v2/*
handle @v1 {
reverse_proxy localhost:8081 {
health_uri /health
health_interval 10s
health_timeout 3s
}
}
handle @v2 {
reverse_proxy localhost:8082 {
health_uri /health
health_interval 10s
}
}
log {
output file /var/log/caddy/api-access.log
format json
}
}
# Frontend
www.example.com {
root * /var/www/frontend/dist
file_server
# SPA için fallback
try_files {path} /index.html
encode gzip
}
# Webhook Handler
webhooks.example.com {
reverse_proxy localhost:8083 {
max_fails 3
fail_duration 30s
unhealthy_status 5xx
}
}
# Metrics - sadece monitoring network'ten erişilebilir
:9090 {
@mon remote_ip 10.10.0.0/24
handle @mon {
metrics /metrics
}
respond 403
}
Bu yapıda Prometheus, caddy_http_requests_total metriğindeki server label’ı sayesinde hangi virtual host’a ne kadar istek gittiğini ayırt edebiliyor. Fakat host label’ı varsayılan olarak metrics’e yansımıyor. Bunu aktif etmek için Caddy’nin trusted_proxies ve log_credentials ayarlarına ek olarak Prometheus’ta metric_relabeling kullanabilirsiniz.
Alertmanager Entegrasyonu
Alert kurallarını yazdık ama bu alertlerin bir yere gitmesi lazım. Docker Compose’a Alertmanager ekleyelim:
cat >> docker-compose.yml << 'EOF'
alertmanager:
image: prom/alertmanager:v0.26.0
container_name: alertmanager
restart: unless-stopped
ports:
- "127.0.0.1:9093:9093"
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
networks:
- monitoring
EOF
Alertmanager yapılandırması:
mkdir -p ~/monitoring/alertmanager
cat > alertmanager/alertmanager.yml << 'EOF'
global:
resolve_timeout: 5m
slack_api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'
route:
group_by: ['alertname', 'server']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'slack-notifications'
routes:
- match:
severity: critical
receiver: 'pagerduty-critical'
continue: true
- match:
severity: warning
receiver: 'slack-notifications'
receivers:
- name: 'slack-notifications'
slack_configs:
- channel: '#infrastructure-alerts'
title: '{{ template "slack.default.title" . }}'
text: |
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Detay:* {{ .Annotations.description }}
*Severity:* {{ .Labels.severity }}
{{ end }}
send_resolved: true
- name: 'pagerduty-critical'
pagerduty_configs:
- routing_key: 'YOUR_PAGERDUTY_ROUTING_KEY'
description: '{{ .GroupLabels.alertname }}: {{ .CommonAnnotations.summary }}'
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'server']
EOF
prometheus.yml dosyasına Alertmanager’ı tanıtın:
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
Stack’i yeniden başlatın:
docker compose down
docker compose up -d
docker compose logs -f --tail=50
Sorun Giderme
Metrikler görünmüyorsa kontrol edilecekler:
# Caddy'nin metrics modülünü yükleyip yüklemediğini kontrol et
caddy list-modules | grep metrics
# Port dinlenip dinlenmediğini kontrol et
ss -tlnp | grep 9090
# Firewall kurallarını kontrol et
sudo ufw status
sudo iptables -L -n | grep 9090
# Caddy log'larını kontrol et
journalctl -u caddy -f
# Prometheus scrape hatalarını kontrol et
curl -s http://localhost:9191/api/v1/targets | python3 -c "
import sys, json
data = json.load(sys.stdin)
for t in data['data']['activeTargets']:
print(t['labels']['job'], t['health'], t.get('lastError', 'OK'))
"
Prometheus’ta “connection refused” hatası alıyorsanız ve Caddy host’ta, Prometheus Docker içindeyse:
# Host IP'sini bul
ip route show default | awk '{print $3}'
# veya
docker network inspect bridge | grep Gateway
Bu IP’yi prometheus.yml‘daki Caddy target’ına yazın.
Sonuç
Caddy ile Prometheus entegrasyonu, düşündüğünüzden çok daha az kompleks bir yapı. Built-in metrik desteği sayesinde ekstra eklenti veya agent olmadan doğrudan scrape edebiliyor, Prometheus’un güçlü PromQL diliyle anlamlı sorgular yazabiliyorsunuz. Grafana ile bunu görselleştirip Alertmanager ile kritik durumlarda bildirim alır hale geldiğinizde elinizde production kalitesinde bir gözlemlenebilirlik stack’i oluyor.
Bir sonraki adım olarak Loki ile log aggregation’ı ve Tempo ile distributed tracing’i bu stack’e ekleyebilirsiniz. Caddy’nin JSON formatında ürettiği access log’ları Loki ile mükemmel bir şekilde çalışıyor ve request ID bazlı trace correlation için oldukça uygun bir zemin sunuyor. Ama bu ayrı bir yazının konusu.