← Kursa Dön
📄 Text · 15 min

Python Logging Modülü Derinlemesine

Kodun çalışıyor, her şey yolunda. Sonra bir gün production'da bir hata oluşuyor. Kullanıcı "çalışmıyor" diyor ama sen ne olduğunu bilmiyorsun. print() satırlarıyla debug etmeye çalışıyorsun — ama production'da print çıktılarını kim görüyor ki? İşte bu noktada logging hayat kurtarır.

Logging, programının çalışma anında ne yaptığını, neyle karşılaştığını ve neyin ters gittiğini kayıt altına almaktır. Python'un yerleşik logging modülü bu işi profesyonel düzeyde yapmanı sağlar. Bu derste logging'i sıfırdan derinlemesine öğreneceksin.


1. print() vs logging

🏥 Analoji: Hasta Takip Sistemi

Bir hastanede doktor, hastanın durumunu iki şekilde takip edebilir:

  1. Yapışkan not: Hastanın yatağına bir post-it yapıştır — "Ateşi var." Kim yazdı, ne zaman yazdı, ne kadar ciddi? Belli değil. Bir süre sonra notlar karışır, kaybolur.

  2. Hasta dosyası: Her kayıt tarih, saat, doktor adı, departman ve ciddiyet seviyesiyle birlikte yazılır. Geçmişe dönük inceleme yapılabilir, departmana göre filtrelenebilir, kritik kayıtlar otomatik alarm tetikler.

print() yapışkan not gibidir — hızlı, basit ama ciddi işlerde yetersiz. logging ise hasta dosyası gibidir — yapılandırılmış, filtrelenebilir, yönlendirilebilir.

Somut Karşılaştırma

# ❌ print ile "loglama"
def process_order(order_id):
    print(f"Sipariş işleniyor: {order_id}")
    try:
        # ... işlem ...
        print(f"Sipariş tamamlandı: {order_id}")
    except Exception as e:
        print(f"HATA! Sipariş {order_id}: {e}")
# ✅ logging ile profesyonel loglama
import logging

logger = logging.getLogger(__name__)

def process_order(order_id):
    logger.info("Sipariş işleniyor: %s", order_id)
    try:
        # ... işlem ...
        logger.info("Sipariş tamamlandı: %s", order_id)
    except Exception as e:
        logger.error("Sipariş işlenemedi: %s", order_id, exc_info=True)

İkinci versiyonda exc_info=True stack trace'i otomatik ekler. Seviye bilgisi (INFO, ERROR) var. Çıktı dosyaya, konsola veya uzak sunucuya yönlendirilebilir. Ve en önemlisi: production'da DEBUG mesajlarını kapatıp sadece ERROR'ları görebilirsin — print ile bu mümkün değil.

print'in Sorunları

Özellikprint()logging
Seviye kontrolü✅ DEBUG→CRITICAL
Çıktı yönlendirmeSadece stdoutDosya, konsol, ağ, e-posta...
Format kontrolüElleOtomatik timestamp, modül adı...
Filtreleme✅ Modül, seviye, özel filtre
Production'da kapatmaSatırları silmen lazımSeviye ayarla, bitti
Thread-safe

2. Logging Temelleri

getLogger() ve Seviyeler

Logging modülünün kalbi Logger nesnesidir. Her modül kendi logger'ını oluşturur:

import logging

# Logger oluştur — modül adıyla
logger = logging.getLogger(__name__)

# Root logger'ı yapılandır (en basit yol)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)

# Farklı seviyelerde log
logger.debug("Detaylı debug bilgisi — geliştirme sırasında")
logger.info("Genel bilgi — normal işlem akışı")
logger.warning("Uyarı — dikkat edilmesi gereken durum")
logger.error("Hata — bir şeyler yanlış gitti")
logger.critical("Kritik — sistem çökebilir")

Çıktı:

2024-01-15 10:30:00,123 - __main__ - DEBUG - Detaylı debug bilgisi — geliştirme sırasında
2024-01-15 10:30:00,123 - __main__ - INFO - Genel bilgi — normal işlem akışı
2024-01-15 10:30:00,124 - __main__ - WARNING - Uyarı — dikkat edilmesi gereken durum
2024-01-15 10:30:00,124 - __main__ - ERROR - Hata — bir şeyler yanlış gitti
2024-01-15 10:30:00,124 - __main__ - CRITICAL - Kritik — sistem çökebilir

Seviye Hiyerarşisi

Seviyeler sayısal değerlerle temsil edilir. Bir seviye ayarladığında, o seviye ve üstü loglanır:

DEBUG (10) → INFO (20) → WARNING (30) → ERROR (40) → CRITICAL (50)
import logging

logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger("app")

logger.debug("Bu GÖRÜNMEZ")     # 10 < 30
logger.info("Bu da GÖRÜNMEZ")   # 20 < 30
logger.warning("Bu görünür")    # 30 >= 30 ✅
logger.error("Bu da görünür")   # 40 >= 30 ✅

Development'ta DEBUG, production'da WARNING veya INFO kullanmak yaygın bir pratiktir. Böylece geliştirme sırasında her detayı görürsün, production'da ise sadece önemli olayları loglarsın.

Log Mesajı Formatlama

String formatlama konusunda logging'in özel bir yaklaşımı var:

import logging

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

user_id = 42
action = "login"

# ✅ Doğru — lazy formatting (% stili)
logger.info("Kullanıcı %s %s yaptı", user_id, action)

# ✅ Kabul edilebilir — f-string
logger.info(f"Kullanıcı {user_id} {action} yaptı")

# ✅ Neden % stili öneriliyor?
# Eğer log seviyesi nedeniyle mesaj YAZILMAYACAKSA,
# % stili formatlama YAPILMAZ — performans kazancı
# f-string ise her zaman formatlanır

% stili formatlama, mesaj loglanmayacaksa string oluşturma maliyetinden kaçınır. Yüksek frekanslı debug loglarında bu fark hissedilir. Ama pratikte f-string de yaygın olarak kullanılır — okunabilirlik açısından daha iyi olduğu durumlar var.


3. Handler'lar: Log Nereye Gider?

Handler'lar log mesajlarının nereye yazılacağını belirler. Bir logger'a birden fazla handler ekleyebilirsin — aynı mesaj hem konsola hem dosyaya gidebilir.

StreamHandler — Konsola Yazma

import logging

logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG)

# Konsol handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)  # Konsolda sadece INFO+

formatter = logging.Formatter(
    "%(asctime)s - %(levelname)s - %(message)s"
)
console_handler.setFormatter(formatter)

logger.addHandler(console_handler)

logger.debug("Bu konsolda görünMEZ")  # DEBUG < INFO
logger.info("Bu konsolda görünür")
logger.error("Bu da konsolda görünür")

Dikkat: logger seviyesi DEBUG ama handler seviyesi INFO. Mesaj önce logger filtresinden geçer, sonra handler filtresinden. Her iki filtreyi de geçen mesajlar yazılır.

FileHandler — Dosyaya Yazma

import logging

logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG)

# Dosya handler
file_handler = logging.FileHandler(
    "app.log",
    encoding="utf-8"  # Türkçe karakterler için önemli
)
file_handler.setLevel(logging.DEBUG)

formatter = logging.Formatter(
    "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s"
)
file_handler.setFormatter(formatter)

logger.addHandler(file_handler)

RotatingFileHandler — Boyut Bazlı Döndürme

Log dosyaları büyür. Gigabyte'larca log dosyası disk alanını tüketir. RotatingFileHandler dosya belirli bir boyuta ulaşınca yeni dosyaya geçer:

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG)

# 5MB'a ulaşınca döndür, en fazla 3 yedek tut
handler = RotatingFileHandler(
    "app.log",
    maxBytes=5 * 1024 * 1024,  # 5 MB
    backupCount=3,
    encoding="utf-8"
)

formatter = logging.Formatter(
    "%(asctime)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)

# Dosyalar: app.log → app.log.1 → app.log.2 → app.log.3 (sonra silinir)

Dosya 5MB'a ulaştığında app.logapp.log.1 olur, yeni app.log oluşturulur. backupCount=3 ile en fazla 3 eski dosya tutulur — yani toplam disk kullanımı yaklaşık 20MB ile sınırlı kalır.

TimedRotatingFileHandler — Zaman Bazlı Döndürme

Bazı durumlarda boyut yerine zamana göre döndürmek istersin — her gün yeni bir log dosyası gibi:

import logging
from logging.handlers import TimedRotatingFileHandler

logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG)

# Her gece yarısı döndür, 30 gün tut
handler = TimedRotatingFileHandler(
    "app.log",
    when="midnight",      # Her gece yarısı
    interval=1,           # 1 günde bir
    backupCount=30,       # 30 günlük log tut
    encoding="utf-8"
)
handler.suffix = "%Y-%m-%d"  # app.log.2024-01-15

formatter = logging.Formatter(
    "%(asctime)s - %(levelname)s - %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)

when parametresinin seçenekleri: "S" (saniye), "M" (dakika), "H" (saat), "D" (gün), "midnight" (gece yarısı), "W0"-"W6" (haftanın günleri).

Çoklu Handler: Konsol + Dosya

Production'da en yaygın senaryo: konsolda kısa bilgi, dosyada detaylı log:

import logging
from logging.handlers import RotatingFileHandler

def setup_logging():
    """Production-grade logging kurulumu."""
    logger = logging.getLogger("myapp")
    logger.setLevel(logging.DEBUG)
    
    # Konsol — sadece WARNING ve üstü
    console = logging.StreamHandler()
    console.setLevel(logging.WARNING)
    console.setFormatter(logging.Formatter(
        "%(levelname)s: %(message)s"
    ))
    
    # Dosya — tüm detaylar
    file_handler = RotatingFileHandler(
        "myapp.log",
        maxBytes=10 * 1024 * 1024,
        backupCount=5,
        encoding="utf-8"
    )
    file_handler.setLevel(logging.DEBUG)
    file_handler.setFormatter(logging.Formatter(
        "%(asctime)s - %(name)s - %(levelname)s - "
        "%(funcName)s:%(lineno)d - %(message)s"
    ))
    
    logger.addHandler(console)
    logger.addHandler(file_handler)
    
    return logger

logger = setup_logging()
logger.debug("Debug — sadece dosyada")
logger.warning("Warning — hem konsolda hem dosyada")

4. Formatter: Log Nasıl Görünür?

Formatter log mesajının formatını belirler. Hangi bilgiler dahil edilecek, nasıl sıralanacak, tarih formatı ne olacak — hepsini formatter kontrol eder.

Format String ve Kullanılabilir Alanlar

import logging

# Detaylı format
formatter = logging.Formatter(
    "%(asctime)s | %(name)-20s | %(levelname)-8s | "
    "%(filename)s:%(lineno)d | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

Sık kullanılan format alanları:

AlanAçıklamaÖrnek
%(asctime)sZaman damgası2024-01-15 10:30:00
%(name)sLogger adımyapp.database
%(levelname)sSeviyeWARNING
%(filename)sDosya adıapp.py
%(funcName)sFonksiyon adıprocess_order
%(lineno)dSatır numarası42
%(message)sLog mesajıSipariş işlendi
%(module)sModül adıapp
%(process)dProcess ID12345
%(thread)dThread ID140735

Custom Formatter

Standart formatter yetmediğinde kendi formatter'ını yazabilirsin:

import logging
import json
from datetime import datetime, timezone


class ColorFormatter(logging.Formatter):
    """Konsol için renkli log formatter."""
    
    COLORS = {
        logging.DEBUG:    "\033[36m",    # Cyan
        logging.INFO:     "\033[32m",    # Yeşil
        logging.WARNING:  "\033[33m",    # Sarı
        logging.ERROR:    "\033[31m",    # Kırmızı
        logging.CRITICAL: "\033[41m",    # Kırmızı arka plan
    }
    RESET = "\033[0m"
    
    def format(self, record):
        color = self.COLORS.get(record.levelno, self.RESET)
        record.levelname = f"{color}{record.levelname}{self.RESET}"
        return super().format(record)


# Kullanım
logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
handler.setFormatter(ColorFormatter(
    "%(asctime)s %(levelname)-18s %(message)s"
))
logger.addHandler(handler)

logger.debug("Debug mesajı")
logger.info("Bilgi mesajı")
logger.warning("Uyarı mesajı")
logger.error("Hata mesajı")
logger.critical("Kritik mesaj")

Renkli formatter geliştirme ortamında çok faydalı — loglar arasında hataları gözle taramak çok kolaylaşır. Ama dosyaya yazarken ANSI renk kodları gereksiz olur — dosya handler'ına normal formatter ata.


5. Logger Hiyerarşisi ve Propagation

Python'da logger'lar bir ağaç yapısı oluşturur. Bu yapıyı anlamak, karmaşık projelerde logging'i doğru yapılandırmanın anahtarıdır.

Hiyerarşi Nasıl Çalışır?

Logger adları nokta ile ayrılan hiyerarşi oluşturur:

root
├── myapp                    # logging.getLogger("myapp")
│   ├── myapp.database       # logging.getLogger("myapp.database")
│   ├── myapp.api            # logging.getLogger("myapp.api")
│   │   └── myapp.api.auth   # logging.getLogger("myapp.api.auth")
│   └── myapp.utils          # logging.getLogger("myapp.utils")

__name__ kullanmak bu hiyerarşiyi otomatik oluşturur. myapp/database/models.py dosyasında logging.getLogger(__name__) yazdığında logger adı myapp.database.models olur.

Propagation

Bir logger'a gelen mesaj, üst logger'lara da iletilir (propagate edilir). Bu varsayılan davranıştır:

import logging

# Root logger'ı yapılandır
logging.basicConfig(
    level=logging.DEBUG,
    format="%(name)s - %(levelname)s - %(message)s"
)

# Alt logger oluştur
parent_logger = logging.getLogger("myapp")
child_logger = logging.getLogger("myapp.database")

# Child'dan log yaz
child_logger.warning("Veritabanı bağlantısı yavaş")

# Bu mesaj:
# 1. child_logger (myapp.database) tarafından işlenir
# 2. parent_logger (myapp) tarafından işlenir (propagation)
# 3. root logger tarafından işlenir (propagation)
# → Tek handler varsa tek kez yazılır, sorun yok

Sorun şu: eğer hem child hem parent logger'da handler varsa, mesaj iki kez yazılır:

import logging

# Root handler
logging.basicConfig(level=logging.DEBUG, format="ROOT: %(message)s")

# myapp logger'a ayrı handler ekle
app_logger = logging.getLogger("myapp")
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("APP: %(message)s"))
app_logger.addHandler(handler)

# Alt logger
db_logger = logging.getLogger("myapp.database")
db_logger.warning("Yavaş sorgu tespit edildi")

# Çıktı (mesaj İKİ kez yazılır):
# APP: Yavaş sorgu tespit edildi
# ROOT: Yavaş sorgu tespit edildi

Bunu önlemek için propagation'ı kapatabilirsn:

app_logger.propagate = False  # Üst logger'a iletme

⚠️ Dikkat: Mükerrer log mesajları görüyorsan, sebebi neredeyse her zaman propagation'dır. Ya handler'ı sadece root logger'a ekle, ya da alt logger'larda propagate = False yap.


6. dictConfig ile Yapılandırma

Büyük projelerde logging'i kod içinde yapılandırmak karmaşıklaşır. dictConfig tüm yapılandırmayı tek bir dict (veya YAML/JSON dosyası) ile yapmana olanak tanır.

Python Dict ile

import logging
import logging.config

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    
    "formatters": {
        "simple": {
            "format": "%(levelname)s - %(message)s"
        },
        "detailed": {
            "format": "%(asctime)s - %(name)s - %(levelname)s - "
                      "%(funcName)s:%(lineno)d - %(message)s",
            "datefmt": "%Y-%m-%d %H:%M:%S"
        },
    },
    
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "level": "WARNING",
            "formatter": "simple",
            "stream": "ext://sys.stdout"
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "DEBUG",
            "formatter": "detailed",
            "filename": "app.log",
            "maxBytes": 10485760,  # 10 MB
            "backupCount": 5,
            "encoding": "utf-8"
        },
    },
    
    "loggers": {
        "myapp": {
            "level": "DEBUG",
            "handlers": ["console", "file"],
            "propagate": False
        },
        "myapp.database": {
            "level": "WARNING",  # DB logları sadece WARNING+
            "handlers": ["file"],
            "propagate": False
        },
    },
    
    "root": {
        "level": "WARNING",
        "handlers": ["console"]
    }
}

# Yapılandırmayı uygula
logging.config.dictConfig(LOGGING_CONFIG)

# Kullanım
logger = logging.getLogger("myapp")
logger.info("Uygulama başladı")

db_logger = logging.getLogger("myapp.database")
db_logger.debug("Bu GÖRÜNMEZ — seviye WARNING")
db_logger.warning("Yavaş sorgu — bu görünür")

disable_existing_loggers: False önemli — bunu True yaparsan, yapılandırma öncesi oluşturulan logger'lar devre dışı kalır. Kütüphanelerin logger'ları sessizleşir ve hata ayıklama zorlaşır.

YAML Dosyasından Yükleme

YAML formatı dict'lerden çok daha okunabilir:

# logging_config.yaml
version: 1
disable_existing_loggers: false

formatters:
  detailed:
    format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    datefmt: "%Y-%m-%d %H:%M:%S"

handlers:
  console:
    class: logging.StreamHandler
    level: INFO
    formatter: detailed
    stream: ext://sys.stdout
  
  file:
    class: logging.handlers.RotatingFileHandler
    level: DEBUG
    formatter: detailed
    filename: app.log
    maxBytes: 10485760
    backupCount: 5
    encoding: utf-8

loggers:
  myapp:
    level: DEBUG
    handlers: [console, file]
    propagate: false

root:
  level: WARNING
  handlers: [console]
import logging.config
import yaml

with open("logging_config.yaml") as f:
    config = yaml.safe_load(f)

logging.config.dictConfig(config)

logger = logging.getLogger("myapp")
logger.info("YAML konfigürasyondan yüklendi")

YAML yaklaşımı yapılandırmayı koddan ayırır. Farklı ortamlar (dev, staging, production) için farklı YAML dosyaları kullanabilirsin — kod değişikliği gerekmez.


7. Filtreleme: Filter Sınıfı

Handler'lar seviyeye göre filtreler ama bazen daha ince ayar gerekir. Belirli bir modülden gelen logları engelleme, hassas veriyi maskeleme veya özel alanlar ekleme gibi işler için Filter sınıfı kullanılır.

Temel Filter

import logging


class ModuleFilter(logging.Filter):
    """Belirli modüllerden gelen logları engeller."""
    
    def __init__(self, excluded_modules):
        super().__init__()
        self.excluded_modules = excluded_modules
    
    def filter(self, record):
        # True → logla, False → engelle
        return record.name not in self.excluded_modules


# Kullanım
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(name)s - %(message)s"))

# Gürültülü modülleri filtrele
handler.addFilter(ModuleFilter(["myapp.healthcheck", "myapp.metrics"]))

logger.addHandler(handler)

Hassas Veri Maskeleme

Production loglarında kredi kartı numarası, şifre veya kişisel bilgi olmamalı:

import logging
import re


class SensitiveDataFilter(logging.Filter):
    """Log mesajlarındaki hassas veriyi maskeler."""
    
    PATTERNS = [
        # Kredi kartı numarası (basit pattern)
        (re.compile(r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b"), "****-****-****-****"),
        # E-posta
        (re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b"), "***@***.***"),
        # Şifre parametresi
        (re.compile(r"password['\"]?\s*[:=]\s*['\"]?[\w!@#$%^&*]+", re.I), "password=***"),
    ]
    
    def filter(self, record):
        msg = record.getMessage()
        for pattern, replacement in self.PATTERNS:
            msg = pattern.sub(replacement, msg)
        record.msg = msg
        record.args = None  # Formatlama tekrarını önle
        return True


# Kullanım
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()
handler.addFilter(SensitiveDataFilter())
handler.setFormatter(logging.Formatter("%(message)s"))
logger.addHandler(handler)

logger.info("Kullanıcı ali@example.com giriş yaptı")
# Çıktı: Kullanıcı ***@***.*** giriş yaptı

logger.info("Kart: 4532-1234-5678-9012 ile ödeme")
# Çıktı: Kart: ****-****-****-**** ile ödeme

8. Yapılandırılmış Loglama (Structured Logging)

Geleneksel log satırları insanlar için okunabilir ama makineler için parse etmesi zordur. Modern sistemlerde loglar ELK Stack (Elasticsearch, Logstash, Kibana), Datadog veya Grafana Loki gibi araçlarla işlenir. Bu araçlar JSON formatında log bekler.

python-json-logger

pip install python-json-logger
import logging
from pythonjsonlogger import jsonlogger

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)

handler = logging.StreamHandler()

# JSON formatter
formatter = jsonlogger.JsonFormatter(
    "%(asctime)s %(name)s %(levelname)s %(message)s",
    rename_fields={
        "asctime": "timestamp",
        "levelname": "level",
    }
)
handler.setFormatter(formatter)
logger.addHandler(handler)

# Standart log
logger.info("Kullanıcı giriş yaptı")
# {"timestamp": "2024-01-15 10:30:00", "name": "myapp", "level": "INFO", "message": "Kullanıcı giriş yaptı"}

# Extra alanlarla
logger.info(
    "Sipariş oluşturuldu",
    extra={
        "order_id": "ORD-123",
        "user_id": "USR-42",
        "amount": 99.99,
        "currency": "TRY"
    }
)
# {"timestamp": "...", "level": "INFO", "message": "Sipariş oluşturuldu",
#  "order_id": "ORD-123", "user_id": "USR-42", "amount": 99.99, "currency": "TRY"}

JSON loglar makineler tarafından kolayca parse edilir. order_id'ye göre filtreleme, amount'a göre toplama gibi sorguları log toplama araçlarında doğrudan çalıştırabilirsin.


9. Production Logging Best Practices

Ortama Göre Yapılandırma

Production'da en iyi pratik: ortama göre (dev/prod) farklı logging yapılandırması kullanmaktır:

import logging
import logging.config
import os

def setup_logging():
    """Ortam değişkenine göre logging yapılandırır."""
    env = os.getenv("APP_ENV", "development")
    os.makedirs("logs", exist_ok=True)
    
    if env == "development":
        logging.basicConfig(
            level=logging.DEBUG,
            format="%(asctime)s [%(levelname)s] %(name)s %(funcName)s:%(lineno)d — %(message)s",
            datefmt="%H:%M:%S"
        )
    else:
        logging.config.dictConfig({
            "version": 1,
            "disable_existing_loggers": False,
            "formatters": {
                "detailed": {
                    "format": "%(asctime)s [%(levelname)s] %(name)s — %(message)s"
                }
            },
            "handlers": {
                "console": {
                    "class": "logging.StreamHandler",
                    "level": "WARNING",
                    "formatter": "detailed",
                },
                "file": {
                    "class": "logging.handlers.RotatingFileHandler",
                    "level": "INFO",
                    "formatter": "detailed",
                    "filename": "logs/app.log",
                    "maxBytes": 50_000_000,
                    "backupCount": 10,
                    "encoding": "utf-8",
                },
            },
            "loggers": {
                "urllib3": {"level": "WARNING"},
                "sqlalchemy.engine": {"level": "WARNING"},
            },
            "root": {"level": "INFO", "handlers": ["console", "file"]},
        })
    
    logging.getLogger(__name__).info("Logging hazır — ortam: %s", env)

setup_logging()

Gerçek Dünya Kullanım Örneği

"""
order_service.py — Logging'i doğru kullanan bir servis örneği.
"""

import logging
import time
from contextlib import contextmanager

logger = logging.getLogger(__name__)


@contextmanager
def log_operation(operation_name, **context):
    """İşlem süresi ve sonucunu loglar."""
    start = time.perf_counter()
    logger.info(
        "%s başladı", operation_name,
        extra=context
    )
    
    try:
        yield
        elapsed = time.perf_counter() - start
        logger.info(
            "%s tamamlandı (%.3fs)", operation_name, elapsed,
            extra=context
        )
    except Exception:
        elapsed = time.perf_counter() - start
        logger.error(
            "%s başarısız (%.3fs)", operation_name, elapsed,
            extra=context,
            exc_info=True  # Stack trace dahil et
        )
        raise


class OrderService:
    def __init__(self, db, payment_gateway):
        self.db = db
        self.payment_gateway = payment_gateway
        self.logger = logging.getLogger(f"{__name__}.OrderService")
    
    def create_order(self, user_id, items):
        self.logger.info(
            "Sipariş oluşturuluyor — kullanıcı: %s, ürün sayısı: %d",
            user_id, len(items)
        )
        
        with log_operation("Stok kontrolü", user_id=user_id):
            self._check_stock(items)
        
        with log_operation("Ödeme işlemi", user_id=user_id):
            total = sum(item["price"] * item["qty"] for item in items)
            self._process_payment(user_id, total)
        
        with log_operation("Sipariş kaydı", user_id=user_id):
            order_id = self._save_order(user_id, items)
        
        self.logger.info(
            "Sipariş tamamlandı — order_id: %s, toplam: %.2f",
            order_id, total
        )
        return order_id
    
    def _check_stock(self, items):
        for item in items:
            self.logger.debug("Stok kontrolü: %s", item["name"])
    
    def _process_payment(self, user_id, amount):
        self.logger.debug("Ödeme: %.2f TL, kullanıcı: %s", amount, user_id)
    
    def _save_order(self, user_id, items):
        self.logger.debug("Veritabanına yazılıyor")
        return "ORD-12345"

Bu örnekte birkaç önemli pratik var:

  • Her sınıf kendi logger'ını oluşturuyor (__name__ + sınıf adı)

  • log_operation context manager'ı işlem süresini otomatik ölçüyor

  • exc_info=True hata durumunda stack trace'i dahil ediyor

  • Debug logları detaylı, info logları anlamlı iş olaylarını kaydediyor

💡 İpucu: Log mesajlarını "5 yıl sonra bu sistemi debuglamaya çalışan bir geliştirici ne bilmek ister?" perspektifinden yaz. "İşlem yapıldı" yerine "Sipariş ORD-123 kullanıcı USR-42 için 99.99 TRY tutarında oluşturuldu" çok daha faydalı.

Sık Yapılan Hatalar

import logging

logger = logging.getLogger(__name__)

# ❌ YANLIŞ: Exception'ı string olarak logla
try:
    result = 1 / 0
except Exception as e:
    logger.error(f"Hata oluştu: {e}")
    # Stack trace YOK — nerede olduğunu bilemezsin

# ✅ DOĞRU: exc_info ile stack trace dahil et
try:
    result = 1 / 0
except Exception as e:
    logger.error("Hata oluştu", exc_info=True)
    # veya daha kısa:
    logger.exception("Hata oluştu")
    # logger.exception() otomatik olarak exc_info=True ekler

# ❌ YANLIŞ: Root logger'ı kirletme
logging.info("Bu root logger'a gider")

# ✅ DOĞRU: Modül logger'ı kullan
logger = logging.getLogger(__name__)
logger.info("Bu modüle özel logger'a gider")

# ❌ YANLIŞ: Her yerde basicConfig çağırma
# (Sadece bir kez, uygulama başlangıcında çağrılmalı)
logging.basicConfig(level=logging.DEBUG)  # İlk çağrıdan sonrası etkisiz

# ✅ DOĞRU: dictConfig veya tek bir setup fonksiyonu kullan

logger.exception() metodu logger.error(..., exc_info=True) ile aynı şeyi yapar — daha kısa ve okunabilir. Sadece bir except bloğu içinde kullan.


Özet

  • print() yerine logging kullan — seviye kontrolü, filtreleme ve çıktı yönlendirme sağlar

  • getLogger(__name__) ile her modül kendi logger'ını oluşturur; bu hiyerarşik yapı modüle özgü yapılandırma imkânı tanır

  • Handler'lar log mesajlarını nereye gönderileceğini belirler; RotatingFileHandler ile disk dolmasını, TimedRotatingFileHandler ile günlük döndürmeyi yönetirsin

  • dictConfig ile tüm yapılandırmayı tek bir yerden (dict, YAML, JSON) yönetmek en temiz yaklaşımdır

  • Structured logging (JSON format) modern log toplama araçlarıyla uyumlu çalışır ve arama/filtrelemeyi kolaylaştırır

  • exc_info=True veya logger.exception() kullanarak stack trace'i her zaman dahil et — hata ayıklama için vazgeçilmez