Python’da Hata Yönetimi ve Loglama: logging Modülü Rehberi

Bir sistemi yönetirken en can sıkıcı anlardan biri şudur: Gece 2’de bir script çökmüş, sabah geliyorsun ve ekranda sadece “bir hata oluştu” yazısı var. Ne zaman oldu? Hangi sunucuda? Hangi işlem sırasında? Hiçbir fikrin yok. İşte tam bu yüzden loglama, otomasyon scriptlerinin en kritik parçasıdır. Python’un built-in logging modülü bu sorunu profesyonelce çözüyor ve bir kez düzgün kurulum yaptıktan sonra hayatını ciddi anlamda kolaylaştırıyor.

Neden print() Yetmez?

Çoğu sysadmin Python öğrenirken debug için print() kullanmaya başlar. Hızlı, basit, anlaşılır. Ama production ortamında bu yaklaşım seni yarı yolda bırakır.

print() ile şunları yapamazsın:

  • Log seviyesi belirleyemezsin (debug mi, kritik hata mı, bilgi mi?)
  • Çıktıyı aynı anda hem dosyaya hem konsola yazamazsın
  • Timestamp otomatik eklenemez
  • Hangi modülden, hangi satırdan geldiğini göremezsin
  • Production’da debug mesajlarını kapatıp sadece hataları gösteremezsin

logging modülü tüm bu eksiklikleri gideriyor. Üstelik standart kütüphanede geliyor, ekstra kurulum yok.

logging Modülünün Temel Yapısı

Modülü anlamak için önce bileşenlerini tanıyalım:

  • Logger: Log mesajlarını yazan ana nesne. Her modül için ayrı logger oluşturabilirsin.
  • Handler: Logların nereye gideceğini belirler. Dosya, konsol, network, email olabilir.
  • Formatter: Log mesajının nasıl görüneceğini formatlar. Timestamp, seviye, mesaj gibi.
  • Filter: Hangi logların geçeceğini filtreler. İnce ayar için kullanılır.

Log seviyeleri ise şöyle sıralanır, düşükten yükseğe:

  • DEBUG: Geliştirme sırasında her şeyi görmek için. 10 numara.
  • INFO: Normal operasyon bilgileri. “Backup başladı”, “Servis yeniden başlatıldı” gibi. 20 numara.
  • WARNING: Sorun değil ama dikkat edilmeli. Disk %80 doldu gibi. 30 numara.
  • ERROR: Bir şeyler yanlış gitti ama script devam edebilir. 40 numara.
  • CRITICAL: Sistem çöküyor, her şey durdu. 50 numara.

Temel Kullanım: İlk Adımlar

En basit haliyle başlayalım:

python3 << 'EOF'
import logging

# Temel konfigürasyon
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)

logging.debug("Bu bir debug mesajı")
logging.info("Servis başarıyla başlatıldı")
logging.warning("Disk kullanımı %85'e ulaştı")
logging.error("Backup dosyası oluşturulamadı")
logging.critical("Veritabanı bağlantısı tamamen koptu")
EOF

Çıktı şöyle görünür:

2024-01-15 14:32:01 - DEBUG - Bu bir debug mesajı
2024-01-15 14:32:01 - INFO - Servis başarıyla başlatıldı
2024-01-15 14:32:01 - WARNING - Disk kullanımı %85'e ulaştı
2024-01-15 14:32:01 - ERROR - Backup dosyası oluşturulamadı
2024-01-15 14:32:01 - CRITICAL - Veritabanı bağlantısı tamamen koptu

Bu bile print()‘ten çok daha bilgilendirici. Ama daha iyisini yapabiliriz.

Profesyonel Logger Kurulumu

Gerçek dünyada kullandığım temel logger yapısını paylaşayım. Bu yapıyı kendi scriptlerimde neredeyse her zaman kullanıyorum:

cat > /opt/scripts/logger_setup.py << 'EOF'
import logging
import logging.handlers
import os
from datetime import datetime

def setup_logger(name, log_file=None, level=logging.INFO):
    """
    Hem konsola hem dosyaya yazan, rotation destekli logger.
    
    Parametreler:
    - name: Logger adı (genelde __name__ kullanılır)
    - log_file: Log dosyasının yolu (None ise sadece konsola yazar)
    - level: Minimum log seviyesi
    """
    logger = logging.getLogger(name)
    logger.setLevel(logging.DEBUG)  # Logger'ı en düşük seviyeye al

    # Format tanımla
    detailed_formatter = logging.Formatter(
        fmt='%(asctime)s | %(name)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    simple_formatter = logging.Formatter(
        fmt='%(asctime)s | %(levelname)-8s | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )

    # Konsol handler - WARNING ve üzeri göster
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.WARNING)
    console_handler.setFormatter(simple_formatter)
    logger.addHandler(console_handler)

    # Dosya handler - rotation ile
    if log_file:
        os.makedirs(os.path.dirname(log_file), exist_ok=True)
        
        file_handler = logging.handlers.RotatingFileHandler(
            log_file,
            maxBytes=10 * 1024 * 1024,  # 10 MB
            backupCount=5,
            encoding='utf-8'
        )
        file_handler.setLevel(level)
        file_handler.setFormatter(detailed_formatter)
        logger.addHandler(file_handler)

    return logger


# Test edelim
if __name__ == "__main__":
    log = setup_logger(
        "disk_monitor",
        log_file="/var/log/scripts/disk_monitor.log",
        level=logging.DEBUG
    )
    
    log.debug("Script başlatıldı, parametreler yüklendi")
    log.info("Disk kullanım kontrolü başlıyor")
    log.warning("/ bölümü %82 dolu")
    log.error("/data bölümüne erişilemiyor")
EOF

python3 /opt/scripts/logger_setup.py

Burada dikkat çeken birkaç nokta var. RotatingFileHandler kullanmak kritik: Log dosyan sonsuza kadar büyümez, 10 MB olunca rotate eder ve 5 yedek tutar. Konsol handler’ı WARNING seviyesinde tutuyorum çünkü script çalışırken terminale debug mesajları yağmasını istemiyorum.

Gerçek Dünya Senaryosu: Disk İzleme Scripti

Teoriden pratiğe geçelim. Birden fazla sunucunun disk kullanımını izleyen ve kritik durumlarda log atan bir script yazalım:

cat > /opt/scripts/disk_monitor.py << 'EOF'
import logging
import logging.handlers
import shutil
import os
import sys
from datetime import datetime

# Logger kurulumu
def get_logger():
    logger = logging.getLogger("disk_monitor")
    logger.setLevel(logging.DEBUG)
    
    if logger.handlers:
        return logger
    
    formatter = logging.Formatter(
        '%(asctime)s | %(levelname)-8s | %(funcName)s | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    # Dosya handler
    fh = logging.handlers.TimedRotatingFileHandler(
        '/var/log/scripts/disk_monitor.log',
        when='midnight',
        interval=1,
        backupCount=30,
        encoding='utf-8'
    )
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(formatter)
    
    # Konsol handler
    ch = logging.StreamHandler(sys.stdout)
    ch.setLevel(logging.INFO)
    ch.setFormatter(formatter)
    
    logger.addHandler(fh)
    logger.addHandler(ch)
    
    return logger


def check_disk_usage(path, warning_threshold=80, critical_threshold=90):
    """Belirtilen path için disk kullanımını kontrol eder."""
    logger = get_logger()
    
    try:
        logger.debug(f"Disk kontrolü başlıyor: {path}")
        usage = shutil.disk_usage(path)
        
        total_gb = usage.total / (1024 ** 3)
        used_gb = usage.used / (1024 ** 3)
        free_gb = usage.free / (1024 ** 3)
        percent = (usage.used / usage.total) * 100
        
        logger.debug(
            f"{path} - Toplam: {total_gb:.1f}GB, "
            f"Kullanılan: {used_gb:.1f}GB, "
            f"Boş: {free_gb:.1f}GB"
        )
        
        if percent >= critical_threshold:
            logger.critical(
                f"KRİTİK: {path} disk kullanımı %{percent:.1f} - "
                f"Sadece {free_gb:.1f}GB boş alan kaldı!"
            )
            return "critical", percent
            
        elif percent >= warning_threshold:
            logger.warning(
                f"UYARI: {path} disk kullanımı %{percent:.1f} - "
                f"{free_gb:.1f}GB boş alan"
            )
            return "warning", percent
            
        else:
            logger.info(
                f"OK: {path} disk kullanımı %{percent:.1f} - "
                f"{free_gb:.1f}GB boş alan mevcut"
            )
            return "ok", percent
            
    except PermissionError:
        logger.error(f"Erişim reddedildi: {path} dizinine erişilemiyor")
        return "error", None
        
    except FileNotFoundError:
        logger.error(f"Dizin bulunamadı: {path} mevcut değil")
        return "error", None
        
    except Exception as e:
        logger.exception(f"Beklenmeyen hata ({path}): {e}")
        return "error", None


def run_disk_checks():
    logger = get_logger()
    
    paths_to_check = ["/", "/home", "/var", "/tmp", "/opt"]
    
    logger.info("=" * 50)
    logger.info(f"Disk kontrol döngüsü başlıyor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    logger.info("=" * 50)
    
    results = {}
    for path in paths_to_check:
        if os.path.exists(path):
            status, percent = check_disk_usage(path)
            results[path] = (status, percent)
        else:
            logger.debug(f"{path} bu sistemde mevcut değil, atlanıyor")
    
    # Özet
    critical_paths = [p for p, (s, _) in results.items() if s == "critical"]
    warning_paths = [p for p, (s, _) in results.items() if s == "warning"]
    
    if critical_paths:
        logger.critical(f"KRİTİK durumda {len(critical_paths)} dizin: {critical_paths}")
    
    if warning_paths:
        logger.warning(f"UYARI durumunda {len(warning_paths)} dizin: {warning_paths}")
    
    logger.info("Disk kontrol döngüsü tamamlandı")
    
    return results


if __name__ == "__main__":
    os.makedirs('/var/log/scripts', exist_ok=True)
    run_disk_checks()
EOF

python3 /opt/scripts/disk_monitor.py

logger.exception() metodunu fark ettiyseniz, bu logger.error()‘dan farklı olarak tam stack trace’i de log’a ekler. Hata debug ederken altın değerinde.

Exception Handling ile Loglama Entegrasyonu

Hata yönetimi ve loglama birbirinden ayrılmaz. İşte profesyonel bir yaklaşım:

cat > /opt/scripts/service_manager.py << 'EOF'
import logging
import subprocess
import sys
import time

logger = logging.getLogger(__name__)

def restart_service(service_name, max_retries=3, wait_seconds=5):
    """
    Sistem servisini yeniden başlatır, başarısız olursa retry yapar.
    Her adım detaylıca loglanır.
    """
    logger.info(f"Servis yeniden başlatma talebi: {service_name}")
    
    for attempt in range(1, max_retries + 1):
        logger.debug(f"Deneme {attempt}/{max_retries}: {service_name} durduruluyor")
        
        try:
            # Servisi durdur
            stop_result = subprocess.run(
                ["systemctl", "stop", service_name],
                capture_output=True,
                text=True,
                timeout=30
            )
            
            if stop_result.returncode != 0:
                logger.warning(
                    f"Servis durdurma uyarısı ({service_name}): "
                    f"returncode={stop_result.returncode}, "
                    f"stderr={stop_result.stderr.strip()}"
                )
            
            time.sleep(2)
            
            # Servisi başlat
            start_result = subprocess.run(
                ["systemctl", "start", service_name],
                capture_output=True,
                text=True,
                timeout=30
            )
            
            if start_result.returncode == 0:
                logger.info(f"Servis başarıyla yeniden başlatıldı: {service_name}")
                
                # Durum kontrolü
                status_result = subprocess.run(
                    ["systemctl", "is-active", service_name],
                    capture_output=True,
                    text=True
                )
                
                service_status = status_result.stdout.strip()
                logger.debug(f"Servis durumu: {service_name} = {service_status}")
                
                if service_status == "active":
                    return True
                else:
                    logger.warning(f"Servis aktif değil: {service_name} durumu '{service_status}'")
                    
            else:
                logger.error(
                    f"Servis başlatılamadı (deneme {attempt}): {service_name} - "
                    f"{start_result.stderr.strip()}"
                )
                
        except subprocess.TimeoutExpired:
            logger.error(f"Zaman aşımı (deneme {attempt}): {service_name} 30 saniyede yanıt vermedi")
            
        except FileNotFoundError:
            logger.critical("systemctl komutu bulunamadı. systemd yüklü mü?")
            return False
            
        except Exception as e:
            logger.exception(f"Beklenmeyen hata (deneme {attempt}): {e}")
        
        if attempt < max_retries:
            logger.info(f"{wait_seconds} saniye bekleniyor, sonra tekrar denenecek...")
            time.sleep(wait_seconds)
    
    logger.critical(
        f"Servis {max_retries} denemeden sonra başlatılamadı: {service_name}"
    )
    return False


# Basit test (gerçek systemd olmadan)
if __name__ == "__main__":
    logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s | %(name)s | %(levelname)-8s | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    # Bu bir test ortamında muhtemelen başarısız olacak
    # ama loglama davranışını görmek için yeterli
    result = restart_service("nginx", max_retries=2, wait_seconds=2)
    
    if result:
        logger.info("İşlem başarıyla tamamlandı")
    else:
        logger.error("İşlem başarısız oldu")
        sys.exit(1)
EOF

python3 /opt/scripts/service_manager.py

Farklı Ortamlar için Yapılandırma: Config Dosyası Yaklaşımı

Büyük projelerde logging konfigürasyonunu kod içine gömmek iyi pratik değil. Ayrı bir config dosyasında tutmak çok daha temiz:

cat > /opt/scripts/logging_config.py << 'EOF'
import logging
import logging.config
import json
import os

# Logging konfigürasyonunu dictionary olarak tanımla
LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "detailed": {
            "format": "%(asctime)s | %(name)s | %(levelname)-8s | %(filename)s:%(lineno)d | %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S"
        },
        "simple": {
            "format": "%(asctime)s | %(levelname)-8s | %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S"
        },
        "json_like": {
            "format": '{"time": "%(asctime)s", "level": "%(levelname)s", "module": "%(module)s", "message": "%(message)s"}',
            "datefmt": "%Y-%m-%dT%H:%M:%S"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "WARNING",
            "formatter": "simple",
            "stream": "ext://sys.stdout"
        },
        "file_debug": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "DEBUG",
            "formatter": "detailed",
            "filename": "/var/log/scripts/app_debug.log",
            "maxBytes": 10485760,
            "backupCount": 3,
            "encoding": "utf-8"
        },
        "file_error": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "ERROR",
            "formatter": "detailed",
            "filename": "/var/log/scripts/app_error.log",
            "maxBytes": 5242880,
            "backupCount": 10,
            "encoding": "utf-8"
        }
    },
    "loggers": {
        "disk_monitor": {
            "level": "DEBUG",
            "handlers": ["console", "file_debug", "file_error"],
            "propagate": False
        },
        "service_manager": {
            "level": "INFO",
            "handlers": ["console", "file_debug", "file_error"],
            "propagate": False
        }
    },
    "root": {
        "level": "WARNING",
        "handlers": ["console"]
    }
}

# Konfigürasyonu uygula
os.makedirs('/var/log/scripts', exist_ok=True)
logging.config.dictConfig(LOGGING_CONFIG)

# Test
disk_logger = logging.getLogger("disk_monitor")
disk_logger.debug("Config dosyasından yüklenen logger çalışıyor")
disk_logger.info("Disk izleme başladı")
disk_logger.warning("Test uyarısı")
disk_logger.error("Test hatası - bu hem debug hem error log'a gider")

print("Konfigürasyon başarıyla yüklendi!")
print(f"Debug log: /var/log/scripts/app_debug.log")
print(f"Error log: /var/log/scripts/app_error.log")
EOF

python3 /opt/scripts/logging_config.py

Bu yapının güzelliği şu: Error logları ayrı bir dosyada tutuluyor. Sabah işe geldiğinde sadece app_error.log‘a bakıyorsun. Boşsa gece her şey yolunda gitmiş demektir.

Context Manager ile Operasyon Loglama

Uzun süren operasyonlar için başlangıç ve bitiş zamanını otomatik loglamak işini kolaylaştırır:

cat > /opt/scripts/operation_logger.py << 'EOF'
import logging
import time
from contextlib import contextmanager
from functools import wraps

logger = logging.getLogger(__name__)

@contextmanager
def log_operation(operation_name, extra_info=None):
    """
    Bir operasyonun başlangıç ve bitiş zamanını otomatik loglar.
    Hata oluşursa exception detaylarını da yakalar.
    """
    start_time = time.time()
    extra = f" ({extra_info})" if extra_info else ""
    
    logger.info(f"BAŞLADI: {operation_name}{extra}")
    
    try:
        yield
        
        elapsed = time.time() - start_time
        logger.info(f"TAMAMLANDI: {operation_name} - Süre: {elapsed:.2f}s{extra}")
        
    except Exception as e:
        elapsed = time.time() - start_time
        logger.error(
            f"BAŞARISIZ: {operation_name} - Süre: {elapsed:.2f}s - Hata: {type(e).__name__}: {e}{extra}"
        )
        raise


def log_function_call(func):
    """Decorator: Fonksiyon çağrılarını otomatik loglar."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        func_logger = logging.getLogger(func.__module__)
        func_logger.debug(f"Çağrıldı: {func.__name__}(args={args}, kwargs={kwargs})")
        
        try:
            result = func(*args, **kwargs)
            func_logger.debug(f"Döndü: {func.__name__} -> {result}")
            return result
            
        except Exception as e:
            func_logger.error(f"Hata: {func.__name__} - {type(e).__name__}: {e}")
            raise
            
    return wrapper


# Kullanım örnekleri
@log_function_call
def backup_database(db_name, backup_path):
    """Simüle edilmiş backup işlemi."""
    logger.info(f"Veritabanı backup başlıyor: {db_name}")
    time.sleep(0.5)  # Simülasyon
    logger.info(f"Backup tamamlandı: {backup_path}")
    return True


if __name__ == "__main__":
    logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s | %(levelname)-8s | %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S'
    )
    
    # Context manager kullanımı
    with log_operation("Haftalık sistem yedekleme", extra_info="server-prod-01"):
        backup_database("production_db", "/backup/prod_20240115.sql")
        time.sleep(0.3)
    
    # Hata senaryosu
    try:
        with log_operation("Kritik veritabanı migrasyonu"):
            raise ConnectionError("Veritabanına bağlanılamıyor: Connection refused")
    except ConnectionError:
        logger.critical("Migrasyon durdu, manual müdahale gerekiyor!")
EOF

python3 /opt/scripts/operation_logger.py

Loglara Bakma ve Analiz Etme

Log yazmak kadar önemli bir şey de logları okuyabilmek. Birkaç pratik komut:

# Son 50 log kaydını göster
tail -n 50 /var/log/scripts/disk_monitor.log

# Sadece ERROR ve CRITICAL logları filtrele
grep -E "ERROR|CRITICAL" /var/log/scripts/app_debug.log

# Son 1 saatin loglarını göster (log formatına göre ayarla)
awk -v d="$(date -d '1 hour ago' '+%Y-%m-%d %H:%M')" '$0 >= d' /var/log/scripts/app_debug.log

# Hangi modülden kaç hata gelmiş sayısını gör
grep "ERROR" /var/log/scripts/app_debug.log | awk -F'|' '{print $2}' | sort | uniq -c | sort -rn

# Canlı log takibi - birden fazla dosyayı aynı anda izle
tail -f /var/log/scripts/app_debug.log /var/log/scripts/app_error.log

# Son 24 saatin kritik hatalarını say
grep "CRITICAL" /var/log/scripts/app_error.log | grep "$(date '+%Y-%m-%d')" | wc -l

Yaygın Hatalar ve Kaçınılması Gerekenler

Yıllar içinde gördüğüm ve kendim de yaptığım bazı hatalar var:

  • Her yerde basicConfig kullanmak: basicConfig() sadece root logger’ı konfigüre eder ve bir kez çalışır. Modüler projelerde her modül için getLogger(__name__) kullan.
  • Handler’ı her çağrıda eklemek: Fonksiyon içinde handler ekleyip her çağrıda aynı handler’ı tekrar ekliyorsun. Başlangıçta if logger.handlers: return logger kontrolü yap.
  • Log rotasyonunu unutmak: /var/log doluyor, sistem çöküyor. RotatingFileHandler veya TimedRotatingFileHandler her zaman kullan.
  • Hassas bilgileri loglamak: Şifreler, API anahtarları, kullanıcı verileri asla loga yazılmaz. password=* maskele veya hiç loglama.
  • Sadece hataları loglamak: Başarılı operasyonları da logla. “Backup başladı” ve “Backup bitti” arasında log yoksa ne olduğunu anlayamazsın.
  • f-string yerine % formatı: logger.debug(f"değer: {hesapla()}") yazdığında DEBUG kapalı olsa bile hesapla() çalışır. Bunun yerine logger.debug("değer: %s", hesapla()) kullan, lazy evaluation sağlar.

Sonuç

logging modülü, Python ile yazdığın her otomasyon scriptinin olmazsa olmaz parçası. Başlangıçta fazla karmaşık gelebilir ama bir kez düzgün bir logger yapısı kurduğunda, kopyala-yapıştır ile tüm projelerine uygulayabiliyorsun.

Pratik önerim şu: Küçük bir logger_utils.py dosyası oluştur, orada setup_logger() fonksiyonunu bir kez iyi yaz ve tüm scriptlerinde bunu import et. Tek bir yerden konfigüre et, her yerde kullan.

Bir şeyi unutma: Gece 2’de çalan telefonu hayal et. “Bir şeyler bozuldu” diyor meslektaşın. O anda detaylı logların varsa problemi dakikalar içinde bulursun. Yoksa saatler süren bir arıza analizi seni bekliyor. Bu fark, logging modülüne harcadığın birkaç saatlik öğrenme süresinin çok ötesinde bir değer taşıyor.

Yorum yapın