Caddy ile Metrics ve Prometheus Entegrasyonu

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.

Yorum yapın