← Kursa Dön
📄 Text · 35 min

Monitoring ve Logging — Prometheus, Grafana, ELK

Production'a deploy ettik, best practice'leri uyguladık. Ama şimdi çok önemli bir soru var: uygulamanın gerçekten sağlıklı çalışıp çalışmadığını nasıl bileceksin? Container'ların CPU ve memory kullanımı normal mi? Hata oranı artıyor mu? Disk doluyor mu? Bu derste monitoring (izleme), logging (kayıt tutma) ve alerting (uyarı) sistemlerini kurarak bu soruların cevaplarını otomatik olarak almanı sağlayacağız.

Bir hastanede yoğun bakım ünitesini düşün. Her hastanın başında monitörler var — kalp atış hızı, tansiyon, oksijen seviyesi sürekli izleniyor. Bir değer anormal olduğunda alarm çalıyor ve doktorlar hemen müdahale ediyor. Ayrıca hastanın tüm tıbbi geçmişi dosyalanıyor — ne zaman ne oldu, hangi ilaç verildi, ne tepki verdi.

Docker dünyasında monitoring (izleme) bu monitörler, logging (kayıt tutma) bu hasta dosyalarıdır. Container'larının CPU kullanımı, bellek tüketimi, ağ trafiği sürekli izlenmeli. Uygulama logları toplanmalı, aranabilir olmalı, analiz edilebilmeli. Monitoring olmadan production'a çıkmak, gösterge paneli olmadan araba sürmek gibidir — ne hızla gittiğini, yakıtın ne kadar kaldığını bilmiyorsun.

Monitoring vs Logging vs Tracing

KavramNe İzler?AraçlarSoru Cevabı
MonitoringMetrikler (CPU, RAM, istek sayısı)Prometheus, Grafana"Sistem sağlıklı mı?"
LoggingOlay kayıtları (hata, uyarı, bilgi)ELK, Loki, Fluentd"Ne oldu?"
Tracingİstek yolculuğu (servisler arası)Jaeger, Zipkin, Tempo"Nerede yavaşladı?"

Bu üçüne birlikte Observability (gözlemlenebilirlik) denir. Production'da üçü de olmalı.

Prometheus — Metrik Toplama

Prometheus, pull-based bir monitoring sistemidir. Container'lardan metrikleri çeker, depolar ve sorgulanmasını sağlar.

Prometheus Nasıl Çalışır?

┌──────────────────────────────────────────────────────────┐
│                     Prometheus                            │
│                                                           │
│  ┌─────────┐    ┌─────────────┐    ┌──────────────────┐ │
│  │ Scraper  │───→│  Time Series │───→│  PromQL Query   │ │
│  │ (pull)   │    │  Database    │    │  Engine          │ │
│  └────┬─────┘    └─────────────┘    └────────┬─────────┘ │
│       │                                       │           │
│  Targets'ları                           API / Grafana    │
│  periyodik olarak                                        │
│  scrape eder                                             │
└───┬───────────────────────────────────────────────────────┘
    │
    ├──── cAdvisor (/metrics) → Container metrikleri
    ├──── Node Exporter (/metrics) → Host metrikleri
    ├──── App (/metrics) → Uygulama metrikleri
    └──── Alertmanager → Alarm yönetimi

Production-Ready Monitoring Stack

# docker-compose.monitoring.yml
services:
  # === Prometheus — Metrik toplama ===
  prometheus:
    image: prom/prometheus:v2.49.0
    volumes:
      - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./monitoring/prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
      - prometheus-data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
      - '--storage.tsdb.retention.size=10GB'
      - '--web.enable-lifecycle'             # HTTP ile config reload
    networks:
      - monitoring
    restart: unless-stopped

  # === Grafana — Dashboard ve Görselleştirme ===
  grafana:
    image: grafana/grafana:10.3.0
    volumes:
      - grafana-data:/var/lib/grafana
      - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
      - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD:-admin}
      GF_USERS_ALLOW_SIGN_UP: "false"
      GF_SERVER_ROOT_URL: https://grafana.example.com
    networks:
      - monitoring
    restart: unless-stopped

  # === cAdvisor — Container metrikleri ===
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:v0.49.1
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
      - /dev/disk/:/dev/disk:ro
    ports:
      - "8080:8080"
    privileged: true
    devices:
      - /dev/kmsg
    networks:
      - monitoring
    restart: unless-stopped

  # === Node Exporter — Host metrikleri ===
  node-exporter:
    image: prom/node-exporter:v1.7.0
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    ports:
      - "9100:9100"
    networks:
      - monitoring
    restart: unless-stopped

  # === Alertmanager — Alarm yönetimi ===
  alertmanager:
    image: prom/alertmanager:v0.27.0
    volumes:
      - ./monitoring/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
    ports:
      - "9093:9093"
    networks:
      - monitoring
    restart: unless-stopped

volumes:
  prometheus-data:
  grafana-data:

networks:
  monitoring:
    driver: bridge

Prometheus Konfigürasyonu

# monitoring/prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

# Alarm kuralları
rule_files:
  - "alerts.yml"

# Alertmanager bağlantısı
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

# Scrape hedefleri
scrape_configs:
  # Prometheus kendisi
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # Container metrikleri (cAdvisor)
  - job_name: 'cadvisor'
    scrape_interval: 10s
    static_configs:
      - targets: ['cadvisor:8080']

  # Host metrikleri
  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  # Docker Engine metrikleri
  - job_name: 'docker'
    static_configs:
      - targets: ['host.docker.internal:9323']

  # Uygulama metrikleri
  - job_name: 'api'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['api:3000']
    scrape_interval: 10s

Alarm Kuralları

# monitoring/prometheus/alerts.yml
groups:
  - name: container_alerts
    rules:
      # Container çok fazla CPU kullanıyor
      - alert: HighCPUUsage
        expr: rate(container_cpu_usage_seconds_total{name!=""}[5m]) * 100 > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Container {{ $labels.name }} yüksek CPU kullanımı"
          description: "CPU kullanımı %{{ $value | printf \"%.1f\" }} (5 dakikadır > %80)"

      # Container bellek limiti yaklaşıyor
      - alert: HighMemoryUsage
        expr: container_memory_usage_bytes{name!=""} / container_spec_memory_limit_bytes{name!=""} * 100 > 85
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Container {{ $labels.name }} bellek limiti yaklaşıyor"
          description: "Bellek kullanımı %{{ $value | printf \"%.1f\" }}"

      # Container restart döngüsünde
      - alert: ContainerRestartLoop
        expr: rate(container_last_seen{name!=""}[5m]) > 3
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "Container {{ $labels.name }} sürekli restart ediyor"

  - name: host_alerts
    rules:
      # Disk alanı azalıyor
      - alert: DiskSpaceLow
        expr: (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 < 15
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Disk alanı azalıyor"
          description: "Kalan alan: %{{ $value | printf \"%.1f\" }}"

      # Host bellek azalıyor
      - alert: HostMemoryLow
        expr: (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 < 10
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "Host bellek kritik seviyede"

Alertmanager Konfigürasyonu

# monitoring/alertmanager/alertmanager.yml
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'severity']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'slack-notifications'

  routes:
    - match:
        severity: critical
      receiver: 'slack-critical'
      repeat_interval: 1h

receivers:
  - name: 'slack-notifications'
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/xxx/yyy/zzz'
        channel: '#monitoring'
        title: '{{ .GroupLabels.alertname }}'
        text: '{{ .CommonAnnotations.description }}'

  - name: 'slack-critical'
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/xxx/yyy/zzz'
        channel: '#critical-alerts'
        title: '🔥 CRITICAL: {{ .GroupLabels.alertname }}'
        text: '{{ .CommonAnnotations.description }}'

Temel PromQL Sorguları

# Container CPU kullanımı (%)
rate(container_cpu_usage_seconds_total{name!=""}[5m]) * 100

# Container bellek kullanımı (MB)
container_memory_usage_bytes{name!=""} / 1024 / 1024

# Container network I/O (bytes/sec)
rate(container_network_receive_bytes_total{name!=""}[5m])
rate(container_network_transmit_bytes_total{name!=""}[5m])

# Container restart sayısı
increase(container_restart_count{name!=""}[1h])

# HTTP istek hızı (uygulama metriği)
rate(http_requests_total[5m])

# HTTP hata oranı
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100

# Response time 95. yüzdelik (percentile)
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

Uygulama Metrikleri — Custom Metrics

Uygulamanın kendi metriklerini Prometheus'a expose etmelisin.

Node.js Örneği

// src/metrics.js
const client = require('prom-client');

// Default metrikleri topla (CPU, memory, event loop)
client.collectDefaultMetrics({ prefix: 'myapp_' });

// Custom metrikler
const httpRequestCounter = new client.Counter({
  name: 'http_requests_total',
  help: 'Total HTTP requests',
  labelNames: ['method', 'path', 'status'],
});

const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'path'],
  buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
});

const activeConnections = new client.Gauge({
  name: 'active_connections',
  help: 'Number of active connections',
});

// Middleware
function metricsMiddleware(req, res, next) {
  const start = Date.now();
  activeConnections.inc();

  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestCounter.inc({
      method: req.method,
      path: req.route?.path || req.path,
      status: res.statusCode,
    });
    httpRequestDuration.observe(
      { method: req.method, path: req.route?.path || req.path },
      duration
    );
    activeConnections.dec();
  });

  next();
}

// /metrics endpoint
async function metricsHandler(req, res) {
  res.set('Content-Type', client.register.contentType);
  res.end(await client.register.metrics());
}

module.exports = { metricsMiddleware, metricsHandler };
// src/app.js
const express = require('express');
const { metricsMiddleware, metricsHandler } = require('./metrics');

const app = express();
app.use(metricsMiddleware);
app.get('/metrics', metricsHandler);
// ... diğer route'lar

Grafana — Dashboard ve Görselleştirme

Datasource Provisioning

# monitoring/grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false

  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    editable: false

Dashboard Provisioning

# monitoring/grafana/provisioning/dashboards/dashboard.yml
apiVersion: 1
providers:
  - name: 'Docker'
    orgId: 1
    folder: 'Docker'
    type: file
    disableDeletion: true
    editable: false
    options:
      path: /var/lib/grafana/dashboards
      foldersFromFilesStructure: true

Önemli Grafana Dashboard'ları

# Docker container monitoring
# Dashboard ID: 893 (Docker and system monitoring)
# Dashboard ID: 14282 (cAdvisor Full)

# Node Exporter
# Dashboard ID: 1860 (Node Exporter Full)

# Grafana'da import:
# + → Import → Dashboard ID gir → Load → Prometheus datasource seç → Import

Logging — Centralized Log Yönetimi

Seçenek 1: Loki + Grafana (Hafif — Önerilen)

Loki, Grafana'nın log toplama aracıdır. Prometheus'un log versiyonu gibi düşün — hafif, verimli, Grafana ile entegre.

# docker-compose.logging.yml
services:
  loki:
    image: grafana/loki:2.9.4
    volumes:
      - ./monitoring/loki/loki-config.yml:/etc/loki/local-config.yaml:ro
      - loki-data:/loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - monitoring
    restart: unless-stopped

  promtail:
    image: grafana/promtail:2.9.4
    volumes:
      - ./monitoring/promtail/promtail-config.yml:/etc/promtail/config.yml:ro
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    command: -config.file=/etc/promtail/config.yml
    networks:
      - monitoring
    restart: unless-stopped

volumes:
  loki-data:
# monitoring/loki/loki-config.yml
auth_enabled: false

server:
  http_listen_port: 3100

common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v12
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 30d
# monitoring/promtail/promtail-config.yml
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  # Docker container logları
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        regex: '/(.*)'
        target_label: 'container'
      - source_labels: ['__meta_docker_container_log_stream']
        target_label: 'logstream'
      - source_labels: ['__meta_docker_container_label_com_docker_compose_service']
        target_label: 'service'

Seçenek 2: ELK Stack (Ağır — Büyük Ölçek)

# docker-compose.elk.yml
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
    volumes:
      - elasticsearch-data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    deploy:
      resources:
        limits:
          memory: 2G
    networks:
      - monitoring
    restart: unless-stopped

  logstash:
    image: docker.elastic.co/logstash/logstash:8.12.0
    volumes:
      - ./monitoring/logstash/pipeline:/usr/share/logstash/pipeline:ro
    ports:
      - "5044:5044"           # Beats input
      - "12201:12201/udp"     # GELF input (Docker log driver)
    environment:
      LS_JAVA_OPTS: "-Xms256m -Xmx256m"
    networks:
      - monitoring
    restart: unless-stopped

  kibana:
    image: docker.elastic.co/kibana/kibana:8.12.0
    ports:
      - "5601:5601"
    environment:
      ELASTICSEARCH_HOSTS: '["http://elasticsearch:9200"]'
    depends_on:
      - elasticsearch
    networks:
      - monitoring
    restart: unless-stopped

volumes:
  elasticsearch-data:
# monitoring/logstash/pipeline/logstash.conf
input {
  gelf {
    port => 12201
    type => "docker"
  }
}

filter {
  # JSON logları parse et
  if [message] =~ /^\{/ {
    json {
      source => "message"
      target => "parsed"
    }
  }

  # Timestamp düzelt
  date {
    match => [ "[parsed][timestamp]", "ISO8601" ]
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "docker-logs-%{+YYYY.MM.dd}"
  }
}
# Docker daemon'ı GELF log driver ile yapılandır
# Container logları otomatik olarak Logstash'e gider
services:
  api:
    image: myapp:v1.0
    logging:
      driver: gelf
      options:
        gelf-address: "udp://localhost:12201"
        tag: "api"

Loki vs ELK Karşılaştırması

ÖzellikLoki + GrafanaELK Stack
Kaynak tüketimiDüşük (~512MB)Yüksek (2-4GB+)
KurulumBasitKarmaşık
Full-text searchLabel bazlı (sınırlı)Tam metin arama ✅
MaliyetDüşükYüksek (disk + RAM)
ÖlçeklenebilirlikİyiÇok iyi
Grafana entegrasyonNative ✅Plugin ile
Öğrenme eğrisiDüşükOrta-yüksek
En iyi kullanımKüçük-orta projelerBüyük ölçek, karmaşık arama

💡 İpucu: Çoğu proje için Loki + Grafana yeterli ve çok daha az kaynak tüketir. ELK'ya sadece full-text search kritikse veya çok büyük ölçekte log işliyorsan geç.

Docker Daemon Metrics

// /etc/docker/daemon.json
{
  "metrics-addr": "0.0.0.0:9323",
  "experimental": true,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
# Daemon restart
sudo systemctl restart docker

# Metrikleri kontrol et
curl http://localhost:9323/metrics

Structured Logging — JSON Log Formatı

Loglarını JSON formatında yaz — parse edilebilir, filtrelenebilir, analiz edilebilir.

// ❌ Düz text log — parse edilemez
console.log('User 123 logged in from 192.168.1.1');

// ✅ Structured JSON log — her alan aranabilir
const log = {
  timestamp: new Date().toISOString(),
  level: 'info',
  message: 'User logged in',
  userId: 123,
  ip: '192.168.1.1',
  userAgent: 'Mozilla/5.0...',
  requestId: 'req-abc-123',
  duration: 45
};
console.log(JSON.stringify(log));
# Python — structlog kullanımı
import structlog

logger = structlog.get_logger()

logger.info(
    "user_logged_in",
    user_id=123,
    ip="192.168.1.1",
    duration=45
)
# Çıktı: {"event": "user_logged_in", "user_id": 123, "ip": "192.168.1.1", ...}

Best Practices

Yap:

  • Prometheus + Grafana + Loki üçlüsünü minimum monitoring stack olarak kur

  • cAdvisor ile container metriklerini, Node Exporter ile host metriklerini topla

  • Alerting kur — metrikler anormal olduğunda bildirim al (Slack, email, PagerDuty)

  • Uygulama custom metrikleri expose et (request rate, error rate, duration)

  • Structured (JSON) logging kullan — düz text log aranmaz

  • Log rotasyonu ayarla — disk dolar (max-size, max-file)

  • Dashboard'ları provisioning ile yönet (Git'te sakla, elle oluşturma)

  • Retention policy tanımla (30 gün metrik, 14 gün log gibi)

Yapma:

  • Monitoring olmadan production'a çıkma — körlemesine uçuş

  • Her şeyi loglama — gereksiz log gürültü yaratır, disk doldurur

  • Alarm eşiklerini çok düşük tutma — alert fatigue (alarm yorgunluğu)

  • ELK Stack'i küçük projede kullanma — overkill, Loki yeterli

  • Log seviyelerini production'da DEBUG bırakma — INFO veya WARN yeterli

  • Grafana şifresini varsayılan (admin/admin) bırakma

  • Monitoring container'larına kaynak limiti koymayı unutma

Özet

  • Monitoring (Prometheus) metrikleri toplar, Logging (Loki/ELK) olayları kaydeder, Tracing (Jaeger) istek yolculuğunu takip eder

  • Prometheus + Grafana çifti endüstri standardı monitoring çözümüdür

  • cAdvisor container metriklerini, Node Exporter host metriklerini Prometheus'a sunar

  • Alertmanager ile metrik eşikleri aşıldığında otomatik bildirim (Slack, email)

  • Loki + Promtail hafif ve etkili log toplama — küçük-orta projeler için ideal

  • ELK Stack (Elasticsearch + Logstash + Kibana) büyük ölçek ve full-text arama için

  • Structured JSON logging ile logları aranabilir ve analiz edilebilir yap

  • Dashboard'ları Git'te sakla (provisioning) ve retention policy tanımla