Python'da logging modülü, programınız çalışırken olan olayları önem derecesiyle (DEBUG, INFO, WARNING, ERROR, CRITICAL) kaydetmenizi ve bu kayıtları konsola, dosyaya ya da başka hedeflere istediğiniz biçimde göndermenizi sağlayan standart kütüphane aracıdır. En kısa doğru kullanım, her modülde logger = logging.getLogger(__name__) ile bir logger oluşturmak, programın giriş noktasında bir kez logging.basicConfig(...) çağırmak ve mesajları logger.info(...) gibi yöntemlerle yazmaktır.

Bu yazıda print() ile logging arasındaki farkı, seviyeleri, basicConfig() ayarlarını, logger hiyerarşisini ve yayılmayı (propagation), handler ve formatter nesnelerini, dosyaya ve dönen dosyalara log yazmayı, dictConfig() ile yapılandırmayı, kütüphane yazarları için kuralları ve sık yapılan hataları ele alıyoruz. Örnekler Python 3.15.2 ile çalıştırıldı. Serinin bütünü için Python öğrenme rehberi sayfasına bakabilirsiniz.

Seviyeler ve varsayılan davranış

Seviyeler ve varsayılan davranış: DEBUG, INFO, WARNING, ERROR
SeviyeSayısal değerNe zaman kullanılır
DEBUG10Ayrıntılı bilgi; genellikle yalnızca sorun ararken ilgilenilir
INFO20Her şeyin beklendiği gibi çalıştığının teyidi
WARNING30Beklenmeyen bir şey oldu ya da yakında sorun çıkabilir; yazılım çalışmaya devam ediyor
ERROR40Daha ciddi bir sorun; yazılım bir işi yapamadı
CRITICAL50Ciddi hata; program çalışmaya devam edemeyebilir

Varsayılan seviye WARNING'dir: başka bir yapılandırma yapılmadıkça yalnızca bu seviye ve üstündeki olaylar izlenir. HOWTO'daki ilk örnekte de görüldüğü gibi, hiçbir ayar yapmadan logging.warning() çağırırsanız mesaj SEVIYE:logger adı:mesaj biçiminde stderr'e yazılır, logging.info() ise hiçbir şey yazmaz.

python
import logging

logging.warning('Disk alanı azalıyor')
logging.info('Bu satır görünmez')

# çıktı: WARNING:root:Disk alanı azalıyor

Modül düzeyindeki logging.warning(), logging.info() gibi fonksiyonlar kök (root) logger üzerinde çalışır ve henüz yapılandırma yapılmamışsa sizin yerinize basicConfig() çağırır. Küçük betikler için bu kullanışlıdır; ama HOWTO, büyük programlarda yapılandırmayı açıkça kontrol etmek ve başka nedenlerle logger oluşturup onun yöntemlerini çağırmanın daha iyi olduğunu belirtir.

Hiç yapılandırma yoksa: "son çare" handler'ı

Kök logger yerine adlandırılmış bir logger kullanır ve hiç yapılandırma yapmazsanız ilginç bir şey olur: mesaj, seviye ya da logger adı olmadan, çıplak olarak görünür. HOWTO'ya göre bu durumda olay lastResort adlı "son çare" handler'ı ile çıkarılır; bu handler stderr'e yazar, biçimlendirme yapmaz ve seviyesi WARNING'dir. Logger'ınızın neden biçimsiz çıktı verdiğini merak ediyorsanız nedeni büyük olasılıkla budur.

python
import logging

logging.getLogger('kutuphane').warning('yapılandırma yok')

# çıktı: yapılandırma yok

basicConfig() ile hızlı yapılandırma

basicConfig(), kök logger'a bir handler ve formatter ekleyerek temel yapılandırmayı tek çağrıda yapar. En sık kullanılan argümanları level (eşik seviye), format (satır biçimi), datefmt (tarih biçimi), filename ve filemode (dosyaya yazmak için), encoding ve stream'dir (hangi akışa yazılacağı). Aşağıdaki örnekte çıktıyı stdout'a yönlendirdik ve seviyeyi DEBUG yaptık; logger adı olarak __name__ kullandığımız için doğrudan çalıştırılan betikte ad __main__ olarak görünüyor.

python
import logging
import sys

logging.basicConfig(level=logging.DEBUG, stream=sys.stdout,
                    format='%(levelname)s | %(name)s | %(message)s')
logger = logging.getLogger(__name__)

logger.debug('ayrıntılı tanı bilgisi')
logger.info('sunucu başlatıldı, port=%d', 8080)
logger.warning('yapılandırma dosyası bulunamadı, varsayılan kullanılıyor')
logger.error('veritabanına bağlanılamadı')
logger.critical('uygulama kapanıyor')
print(logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)

# çıktı: DEBUG | __main__ | ayrıntılı tanı bilgisi
# çıktı: INFO | __main__ | sunucu başlatıldı, port=8080
# çıktı: WARNING | __main__ | yapılandırma dosyası bulunamadı, varsayılan kullanılıyor
# çıktı: ERROR | __main__ | veritabanına bağlanılamadı
# çıktı: CRITICAL | __main__ | uygulama kapanıyor
# çıktı: 10 20 30 40 50

Biçim dizesinde en sık kullanılan alanlar %(asctime)s (olayın zamanı), %(levelname)s (seviye adı), %(name)s (logger adı) ve %(message)s'dir (mesajın kendisi). HOWTO'ya göre asctime varsayılan olarak ISO 8601 ya da RFC 3339'a benzer bir biçimde gösterilir ve datefmt argümanı time.strftime() ile aynı biçim kodlarını kabul eder. Tarih biçim kodları için Python datetime yazısına bakabilirsiniz. Diğer alanların tam listesi LogRecord özniteliklerinde yer alır.

basicConfig() hakkında bilinmesi gereken üç kural vardır. Birincisi, belgeye göre kök logger'da zaten handler varsa hiçbir şey yapmaz; ikinci çağrı sessizce etkisiz kalır. Python 3.8'de eklenen force=True argümanı mevcut handler'ları kaldırıp kapatarak yeniden yapılandırmayı sağlar. İkincisi, HOWTO'ya göre logger yöntemleri çağrılmadan önce yapılmalıdır. Üçüncüsü, belge bu fonksiyonun diğer iş parçacıkları başlatılmadan önce ana iş parçacığından çağrılması gerektiğini not eder.

python
import logging
import sys

logging.basicConfig(level=logging.WARNING, stream=sys.stdout, format='%(levelname)s %(message)s')
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
logging.info('görünmez: ikinci basicConfig etkisiz')
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout, format='%(levelname)s %(message)s', force=True)
logging.info('force=True sonrası görünür')

seviye = 'debug'
sayisal = getattr(logging, seviye.upper(), None)
print(sayisal, logging.getLevelName(sayisal))

# çıktı: INFO force=True sonrası görünür
# çıktı: 10 DEBUG

Örneğin son satırları, seviyeyi bir komut satırı argümanından almanın HOWTO'da önerilen yolunu gösteriyor: getattr(logging, seviye.upper(), None) sayısal değeri verir; sonuç int değilse geçersiz bir seviye girilmiştir ve ValueError yükseltmek uygundur. Komut satırı argümanlarını almak için Python argparse yazısına bakabilirsiniz.

Python 3.15: basicConfig(formatter=...)

logging belgesine göre Python 3.15'te basicConfig() bir formatter argümanı kazandı: hazır bir Formatter nesnesini doğrudan verebilirsiniz ve bu nesne oluşturulan bütün handler'lara atanır. formatter ile birlikte format, datefmt ya da style verirseniz bu argümanlar anlamını yitireceği için ValueError yükselir. Örnekte style='{' kullanılıyor; bu ayar yalnızca biçim dizesini etkiler, mesaja eklenen değişkenler yine % tarzıyla birleştirilir.

python
import logging
import sys

bicim = logging.Formatter('{levelname:<8} {name}: {message}', style='{')
logging.basicConfig(level=logging.INFO, stream=sys.stdout, formatter=bicim)
logging.getLogger('siparis').info('sipariş %s kaydedildi', 1042)

try:
    logging.basicConfig(force=True, formatter=bicim, format='%(message)s')
except ValueError as e:
    print(type(e).__name__ + ':', e)

# çıktı: INFO     siparis: sipariş 1042 kaydedildi
# çıktı: ValueError: 'format' should not be specified together with 'formatter'

Değişken veriyi loglamak: % tarzı ve tembel biçimlendirme

Değişken veriyi loglamak: % tarzı ve tembel biçimlendirme

Mesaja değişken eklemek için HOWTO, biçim dizesini ve değerleri ayrı argümanlar olarak vermeyi gösterir: logger.warning('%s kullanıcısı %d kez denedi', ad, sayi). Bu eski % biçimlendirme tarzı geriye uyumluluk içindir. Önemli avantajı şudur: belgeye göre mesaj argümanlarının biçimlendirilmesi kaçınılmaz olana kadar ertelenir. Seviye kapalıysa birleştirme hiç yapılmaz. Aşağıdaki örnekte DEBUG mesajı kapalı olduğu için nesnenin __str__ yöntemi hiç çağrılmıyor.

python
import logging
import sys

class Pahali:
    def __str__(self):
        print('__str__ çağrıldı')
        return 'pahalı değer'

logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(levelname)s %(message)s')
log = logging.getLogger('demo')

log.debug('değer: %s', Pahali())
log.info('değer: %s', Pahali())
print(log.isEnabledFor(logging.DEBUG))

# çıktı: __str__ çağrıldı
# çıktı: INFO değer: pahalı değer
# çıktı: False

Dikkat: biçimlendirme ertelenir ama argümanların hesaplanması ertelenmez. Pahali() nesnesi yine de oluşturuldu; yalnızca metne çevrilmedi. Argümanı hesaplamak pahalıysa (örneğin bir veritabanı sorgusu), HOWTO'nun Optimization bölümündeki gibi önce logger.isEnabledFor(logging.DEBUG) ile kontrol edin. f-string kullanırsanız (logger.debug(f'...{x}')) metin, seviye kapalı olsa bile her seferinde oluşturulur; bu yüzden log çağrılarında % tarzı argümanlar tercih edilir. f-string'lerin kendisi Python f-string biçimlendirme yazısında anlatılıyor.

İstisnaları loglamak: logger.exception()

logger.exception() yöntemi ERROR seviyesinde bir mesaj yazar ve mesaja istisna bilgisini (traceback) ekler. Belgeye göre bu yöntem yalnızca bir istisna işleyicisinin, yani except bloğunun içinden çağrılmalıdır. Traceback'i elle biçimlendirmenize gerek kalmaz. Aşağıdaki çıktıda uzun dosya yolunu ornek.py olarak kısalttık; başka bir şey değiştirmedik.

python
import logging
import sys

logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(levelname)s %(message)s')
log = logging.getLogger('odeme')

try:
    1 / 0
except ZeroDivisionError:
    log.exception('tutar hesaplanamadı')

# çıktı: ERROR tutar hesaplanamadı
# çıktı: Traceback (most recent call last):
# çıktı:   File "ornek.py", line 8, in <module>
# çıktı:     1 / 0
# çıktı:     ~~^~~
# çıktı: ZeroDivisionError: division by zero

Logger, handler, formatter: bileşenler

HOWTO'nun ileri bölümüne göre logging kütüphanesi modüler bir yapıya sahiptir. Logger'lar uygulama kodunun doğrudan kullandığı arayüzdür. Handler'lar, logger'ların ürettiği kayıtları uygun hedefe gönderir. Filter'lar hangi kaydın çıkacağını daha ince ayarlamaya yarar. Formatter'lar ise kaydın son çıktıdaki düzenini belirler. Olay bilgisi bu bileşenler arasında bir LogRecord nesnesiyle taşınır.

Logger adları noktalarla ayrılmış bir hiyerarşi oluşturur: uygulama adlı logger, uygulama.db ve uygulama.api logger'larının ebeveynidir. Hiyerarşinin tepesinde kök logger vardır. Bu yüzden HOWTO her modülde logging.getLogger(__name__) kullanmayı önerir; logger adları böylece paket ve modül yapısını izler ve bir olayın nereden geldiği addan anlaşılır.

Yayılma (propagate) ve çift satır sorunu

Bir logger'a gelen olay, önce o logger'ın kendi handler'larına, sonra sırasıyla ata logger'ların handler'larına iletilir. logging belgesine göre olay ata logger'ların handler'larına doğrudan gönderilir; ata logger'ların seviye ve filter ayarları dikkate alınmaz. Bir logger'ın propagate özniteliği False ise zincir orada durur. Aşağıdaki örnekte uygulama.db logger'ının kendi handler'ı yok; mesaj ebeveyn uygulama logger'ının handler'ından çıkıyor. Kök logger'a da handler eklenince aynı mesaj iki kez görünüyor; propagate = False ile tekrar bire iniyor.

python
import logging
import sys

uygulama = logging.getLogger('uygulama')
uygulama.setLevel(logging.INFO)
konsol = logging.StreamHandler(sys.stdout)
konsol.setFormatter(logging.Formatter('[%(name)s] %(levelname)s: %(message)s'))
uygulama.addHandler(konsol)

db = logging.getLogger('uygulama.db')
db.info('sorgu çalıştı')
print(db.parent.name, db.getEffectiveLevel())

logging.basicConfig(stream=sys.stdout, format='KÖK %(name)s: %(message)s')
db.info('aynı mesaj iki kez')

uygulama.propagate = False
db.info('artık bir kez')

# çıktı: [uygulama.db] INFO: sorgu çalıştı
# çıktı: uygulama 20
# çıktı: [uygulama.db] INFO: aynı mesaj iki kez
# çıktı: KÖK uygulama.db: aynı mesaj iki kez
# çıktı: [uygulama.db] INFO: artık bir kez

Çift satır sorunu gerçek projelerde sık görülür: hem bir alt logger'a hem kök logger'a handler eklenir ya da aynı handler bir fonksiyon her çağrıldığında yeniden eklenir. Genel kural, handler'ları hiyerarşide tek bir yerde (çoğu zaman yalnızca kökte ya da uygulamanın üst logger'ında) ve programın başlangıcında bir kez eklemektir. Örnekteki getEffectiveLevel() çağrısı da kullanışlı bir ayrıntıyı gösteriyor: kendi seviyesi ayarlanmamış bir logger, seviyesini ebeveyninden alır.

Dosyaya log yazmak ve dosya döndürme

En basit yol basicConfig(filename='uygulama.log', encoding='utf-8', level=logging.INFO) çağrısıdır. HOWTO'ya göre program her çalıştığında mesajlar dosyanın sonuna eklenir; her çalıştırmada temiz bir dosya istiyorsanız filemode='w' verin. encoding argümanı Python 3.9'da eklendi; Türkçe karakterli mesajlar için açıkça 'utf-8' vermek iyi bir alışkanlıktır.

Uzun süre çalışan programlarda log dosyası sürekli büyür. logging.handlers.RotatingFileHandler, belirli bir boyuta (maxBytes) yaklaşıldığında dosyayı kapatıp yenisini açar ve eskilerini .1, .2 gibi uzantılarla saklar. Belgeye göre maxBytes ya da backupCount sıfırsa döndürme hiç yapılmaz; bu yüzden backupCount en az 1, maxBytes sıfırdan büyük olmalıdır. Yazılan dosya her zaman temel addır (örneğin app.log). Zamana göre döndürme için aynı modülde TimedRotatingFileHandler bulunur.

python
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path

dosya = Path(__file__).with_name('uygulama.log')
log = logging.getLogger('dosya_ornek')
log.setLevel(logging.INFO)
isleyici = RotatingFileHandler(dosya, maxBytes=1_000_000, backupCount=3, encoding='utf-8')
isleyici.setFormatter(logging.Formatter('%(levelname)s:%(name)s:%(message)s'))
log.addHandler(isleyici)

log.info('Kullanıcı Şükrü giriş yaptı')
log.warning('İzmir şubesi yanıt vermiyor')
isleyici.close()
print(dosya.read_text(encoding='utf-8'), end='')
dosya.unlink()

# çıktı: INFO:dosya_ornek:Kullanıcı Şükrü giriş yaptı
# çıktı: WARNING:dosya_ornek:İzmir şubesi yanıt vermiyor

Örnekteki dosya yolu pathlib ile oluşturuldu; ayrıntılar Python pathlib yazısında. Sunucuda log dosyalarının izinleri ve konumu için Linux nedir yazısı temel bir başlangıç sunar.

dictConfig() ile yapılandırma

HOWTO'ya göre logging üç şekilde yapılandırılabilir: logger, handler ve formatter nesnelerini kodla tek tek oluşturmak, bir yapılandırma dosyasını fileConfig() ile okumak ya da bir sözlüğü dictConfig() fonksiyonuna vermek. Sözlük yöntemi, ayarları JSON ya da YAML gibi bir dosyadan okuyup uygulamanın kodunu değiştirmeden log davranışını ayarlamanızı sağlar. Sözlükte version anahtarı zorunludur ve değeri 1'dir.

python
import logging
import logging.config
import sys

AYAR = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'kisa': {'format': '%(levelname)s %(name)s: %(message)s'},
    },
    'handlers': {
        'konsol': {
            'class': 'logging.StreamHandler',
            'formatter': 'kisa',
            'stream': 'ext://sys.stdout',
        },
    },
    'loggers': {
        'uygulama': {'level': 'DEBUG', 'handlers': ['konsol'], 'propagate': False},
    },
    'root': {'level': 'WARNING'},
}

logging.config.dictConfig(AYAR)
logging.getLogger('uygulama.api').debug('istek alındı')
logging.getLogger('baska').info('kök WARNING olduğu için görünmez')

# çıktı: DEBUG uygulama.api: istek alındı

disable_existing_loggers ayarına dikkat edin: logging.config belgesine göre bu anahtar verilmezse varsayılan değeri True'dur ve yapılandırma anında var olan kök dışındaki logger'lar devre dışı kalır. Modüllerinizi dictConfig() çağrısından önce içe aktardıysanız, onların logger'ları sessizce susabilir. Bu yüzden örnekte açıkça False verdik. Belgeye göre geçersiz bir seviye ya da boolean olmayan bir propagate değeri gibi hatalarda dictConfig() açıklayıcı bir ValueError, TypeError, AttributeError ya da ImportError yükseltir. JSON dosyalarını okumak için Python json modülü yazısına bakabilirsiniz.

Kütüphane yazıyorsanız

HOWTO'nun "Configuring Logging for a Library" bölümü kütüphane yazarlarına üç öneri verir. Birincisi, kütüphaneniz kök logger'a log yazmamalı; paketinizin adını taşıyan benzersiz bir logger (örneğin __name__) kullanmalı. İkincisi, kütüphanenizin logger'larına NullHandler dışında handler eklememelisiniz; handler seçimi, hedef kitlesini ve ihtiyaçlarını bilen uygulama geliştiricisinin işidir. Üçüncüsü, uygulama logging'i yapılandırmadığında mesajlarınızın stderr'e çıkmasını istemiyorsanız üst logger'ınıza bir NullHandler ekleyebilirsiniz.

python
import logging

logging.getLogger('benim_kutuphanem').addHandler(logging.NullHandler())
logging.getLogger('benim_kutuphanem.ag').warning('kullanıcı yapılandırmadıkça sessiz')
print('çıktı yok')

# çıktı: çıktı yok

HOWTO, yapılandırma yokken WARNING ve üstü mesajların stderr'e yazılmasını en iyi varsayılan davranış olarak görür; yani NullHandler eklemek zorunlu değildir, bilinçli bir tercihtir. Kendi paketinizi yapılandırmak için Python modül ve paket import yazısına, test yazmak için de Python unittest yazısına bakabilirsiniz. unittest modülündeki assertLogs(), bir kod bloğunun beklenen log kaydını üretip üretmediğini doğrular; Python 3.15 What's New belgesine göre bu yöntem artık mesajların nasıl biçimlendirileceğini belirleyen bir formatter da kabul ediyor.

Sürüm notları ve kullanımdan kaldırılanlar

Logger'ın warn() yöntemi Python 3.3'ten beri kullanımdan kaldırılmış durumdadır; What's New belgeleri bunun yerine warning() kullanılmasını hatırlatır. Python 3.15 What's New belgesine göre özel handler'ları strm argümanıyla tanımlamak kullanımdan kaldırıldı ve Python 3.16'da kaldırılması planlanıyor; handler'larınızı stream argümanıyla tanımlayın. Python 3.14'te ise logging.handlers.QueueListener nesneleri context manager protokolünü desteklemeye başladı ve zaten başlatılmış bir dinleyicide start() çağrısı artık RuntimeError yükseltiyor. Diğer değişiklikler için Python 3.15 yenilikleri yazısına bakabilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: INFO mesajlarının görünmemesi, basicConfig'in ikinci kez çağrılması, Aynı mesajın iki kez yazılması, Log çağrısında f-string
  • INFO mesajlarının görünmemesi: Varsayılan seviye WARNING'dir; basicConfig(level=logging.INFO) ya da logger.setLevel() ile ayarlayın.
  • basicConfig'in ikinci kez çağrılması: Kök logger'da handler varsa etkisizdir; gerekiyorsa force=True kullanın.
  • Aynı mesajın iki kez yazılması: Handler hem alt hem üst logger'a eklenmiştir ya da defalarca eklenmiştir; tek yerde bir kez ekleyin veya propagate ayarını gözden geçirin.
  • Log çağrısında f-string: Seviye kapalıyken bile metin oluşturulur; logger.info('%s', deger) yazın.
  • except bloğunda error() ile traceback'i kaybetmek: logger.exception() kullanın ya da exc_info=True verin.
  • Kütüphanede basicConfig çağırmak veya kök logger'a yazmak: Yapılandırma uygulamanın işidir.
  • dictConfig sonrası susan logger'lar: disable_existing_loggers varsayılan olarak True'dur.

Sık Sorulan Sorular

logging iş parçacıklarıyla güvenli mi?

Evet; belgeye göre logging modülü, istemcilerin ek bir şey yapmasına gerek kalmadan iş parçacığı açısından güvenli olacak şekilde tasarlanmıştır ve bunu kilitlerle sağlar: modülün paylaşılan verisi için bir kilit vardır, her handler da kendi G/Ç erişimi için ayrı bir kilit oluşturur. Biçim dizesine %(threadName)s ekleyerek mesajın hangi iş parçacığından geldiğini görebilirsiniz. Birden çok süreçten tek bir dosyaya log yazmak ise Logging Cookbook'a göre desteklenmez; Cookbook bu durum için kayıtları tek bir süreçte toplayan çözümler anlatır. İş parçacıkları ve süreçler Python threading ve multiprocessing yazısında.

Log mesajlarına tarih ve saat nasıl eklenir?

Biçim dizesine %(asctime)s ekleyin. Biçimi değiştirmek için datefmt argümanına time.strftime() biçim kodlarını verin, örneğin '%d.%m.%Y %H:%M:%S'.

Hangi seviyeyi seçmeliyim?

Geliştirme sırasında DEBUG, canlı ortamda genellikle INFO ya da WARNING kullanılır. Seviyeyi koda gömmek yerine bir ortam değişkeninden veya komut satırı argümanından okumak, sorun anında kodu değiştirmeden ayrıntıyı artırmanızı sağlar. Ortam değişkenleri için .env dosyası yazısına bakabilirsiniz.

print() çağrılarını logging'e çevirmeli miyim?

Kullanıcıya gösterilen olağan çıktı print() ile kalmalıdır. Tanı ve izleme amacıyla eklediğiniz print() satırlarını ise logger çağrılarına çevirmek, onları silmeden kapatıp açabilmenizi ve zaman, seviye, modül adı gibi bilgileri otomatik eklemenizi sağlar.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Python öğrenme rehberi · try except hata yönetimi · argparse · unittest · threading ve multiprocessing · asyncio