14.3.1. Günlükleme¶
Sevk edilen bir ürün print() ile eve telefon edemez. print(), yalnızca kamera bir geliştiricinin tezgahında ve bir terminal açıkken var olan USB stdout’a yazar. Sahada bunu kimse okumaz; her satır yere düşürülür. logging kütüphanesi bunun yerine geçer – bir seviye filtresi, uygulamanın seçtiği bir hedef ve ne zaman ne olduğunu söyleyen bir biçim.
Kameradaki logging modülü, CPython’unkinin sadeleştirilmiş bir portudur – aynı zihinsel model, daha küçük bir yüzey ve üretim kurulumu için önemli olan birkaç fark.
14.3.1.1. Zihinsel model¶
Günlükleme dört parçadan oluşur. Her parçanın tek bir işi vardır; bir günlükleyicinin her biri kendi biçimi ve seviyesiyle birden fazla hedefe dağılmasına olanak tanıyan şey bu ayrımdır:
Logger (günlükleyici), uygulamanın çağırdığı şeydir. Kod
log.info("frame %d", n)der; günlükleyici, bu çağrının düştüğü nesnedir. Günlükleyicilerlogging.getLogger()ile ada göre aranır.Handler (işleyici), bir kaydın nereye gideceğine karar verir. Bir
StreamHandlerbir akışa yazar (varsayılan olaraksys.stderr); birFileHandlerdiskteki bir dosyaya ekler. Bir günlükleyici herhangi bir sayıda işleyiciye sahip olabilir.Not
Kamerada
sys.stdoutvesys.stderraynı USB CDC borusuna bağlıdır – her ikisine de yapılan yazmalar, bir geliştiricinin USB üzerinden açık tuttuğu aynı terminalde görünür.sys.stderr‘e yazan bir işleyici, pratikteprint()‘in yazdığı yerle aynı yere yazan bir işleyicidir. İşleyici soyutlaması yine de size hedef başına filtreleme ve biçimlendirme sağlar; sadece fiziksel olarak ayrı bir kanal sağlamaz.Formatter (biçimlendirici), bir kaydın metne nasıl dönüştürüleceğine karar verir. Bir kaydı alır ve yazılacak satırı döndürür. Biçimlendirici başına bir biçim dizesi; işleyici başına bir biçimlendirici.
Her günlükleyicide ve her işleyicide bir seviye filtresi bulunur. Kayıtlar bir seviye taşır (
DEBUG/INFO/WARNING/ERROR/CRITICAL). Yalnızca filtre seviyesinde veya üzerindeki kayıtlar geçer.
Bu ayrım önemlidir çünkü tipik bir üretim kurulumunun birden fazla hedefi vardır: sonradan analiz için her şeyi DEBUG seviyesine kadar tutan SD karttaki bir dosya ve kameraya bağlı bir geliştiricinin ayrıntıya boğulmadan önemli noktaları görmesi için yalnızca WARNING ve daha kötülerini yüzeye çıkaran USB’ye giden bir akış. Aynı kod, iki hedef, iki filtre.
14.3.1.2. Seviyeler ve her birinin anlamı¶
Beş seviye sıralı bir ölçektir. Kayıtlar bir seviye taşır, böylece her işleyicideki filtre umursamadıklarını eleyebilir.
DEBUG– izleme, çerçeve başına sayaçlar, dahili durum dökümleri. En düşük seviye; hacim yüksektir.INFO– normal operasyonel olaylar. Wi-Fi bağlandı, bir model yüklendi, bekçi (watchdog) başlatıldı, yeni bir günlük dosyası devreye girdi.WARNING– beklenmedik bir şey ama uygulama bunu hallettı. Düşürülen bir çerçeve, yeniden denenen bir ağ isteği.ERROR– bir işlem başarısız oldu ve uygulama onu tamamlayamadı. Eksik bir model dosyası, reddedilen bir SD kart yazması.CRITICAL– uygulama hiç devam edemez. Bellek yetersizliği, eksik zorunlu bir bağlama noktası.
Hatırlanması gereken önemli bir varsayılan: kameranın logging modülü her günlükleyiciyi WARNING seviyesinde başlatır. DEBUG ve INFO seviyesindeki kayıtlar, Logger.setLevel() çağrılmadıkça – genellikle aşağıdaki basicConfig() çağrısının bir parçası olarak – sessizce düşürülür. “Çalışmayan” bir günlükleme kurulumunun yaygın ilk belirtisi, uygulamanın INFO seviyesinde yayın yapması ve varsayılan filtrenin kaydı yemesidir.
Not
Seviye, kameranın logging‘inin sunduğu tek filtredir. Daha zengin kayıt başına kurallar için Filter nesneleri yoktur; bir kaydın seviyesi geçerse, yayınlanır.
14.3.1.3. basicConfig: hızlı başlangıç¶
logging.basicConfig(), kök günlükleyiciyi tek bir çağrıda yapılandırır. En çok iki biçim görünür:
INFO seviyesinde her şeyi USB stderr’e gönderen bir geliştirme kurulumu:
import logging
logging.basicConfig(level=logging.INFO)
Zaman damgalı bir biçimle her şeyi SD karttaki bir dosyaya gönderen bir üretim kurulumu:
import logging
logging.basicConfig(
filename='/sdcard/logs/app.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)
Bir FileHandler için filename= veya bir StreamHandler için stream= parametresini geçirin; bu ikisi basicConfig() içinde birbirini dışlar.
Biçim dizesi bir %(field)s tarzı şablondur. Kameranın biçimlendiricisinin desteklediği alanlar:
%(asctime)s–time.localtime()‘dan biçimlendirilmiş zaman damgası. Varsayılan biçim%Y-%m-%d %H:%M:%S‘dir; geçersiz kılmak içindatefmt=geçirin.%(levelname)s–DEBUG/INFO/WARNING/ERROR/CRITICAL.%(name)s– günlükleyicinin adı (sonraki bölüme bakın).%(message)s– kaydın biçimlendirilmiş mesajı.%(msecs)d– kaydın zaman damgasının milisaniye kesri.
Hiçbiri verilmezse varsayılan biçim %(levelname)s:%(name)s:%(message)s‘dir – bu geliştirme kurulumu için uygundur ancak bir saha günlüğü için yetersizdir, çünkü dosyayı haftalar sonra yararlı kılan şey zaman damgasıdır.
basicConfig(), force=True geçirilmedikçe sonraki çağrılarda hiçbir şey yapmaz. Başlangıçta bir kez yapılandırın; çalışma ortasında “hedefleri değiştirmek” için onu yeniden çağırmayın.
Not
Kameranın logging‘inde dictConfig() veya fileConfig() yoktur. Yapılandırma her zaman programatiktir – main.py‘den bir kez çağrılan tek bir setup_logging() yardımcısı geleneksel yöntemdir.
14.3.1.4. Modül başına adlandırılmış günlükleyiciler¶
Uygulama kodu modül düzeyindeki kısayolları (logging.info(), logging.warning() vb.) çağırmamalıdır. Bunların hepsi kök günlükleyiciden geçer ve ortaya çıkan günlük kayıtları root adını taşır – kaydın nereden geldiğini söylemek için işe yaramaz.
Gelenek, modülden sonra adlandırılan modül başına bir günlükleyicidir:
# in app/detector.py
import logging
log = logging.getLogger(__name__)
def detect(frame):
log.info("detect on %dx%d frame", frame.width(), frame.height())
Her kayıt daha sonra %(name)s içinde app.detector‘ı taşır ve günlük satırı onu kimin yayınladığını söyler.
Kameranın logging‘i CPython’dan önemli bir noktada farklılık gösterir: günlükleyici ad alanı düzdür. getLogger('app') ve getLogger('app.detector'), ebeveyn/çocuk ilişkisi olmayan bağımsız günlükleyicilerdir – app üzerinde bir seviye ayarlamak, app.detector‘a yayılmaz. İşe yarayan mekanizma şudur: kendi işleyicileri olmayan adlandırılmış bir günlükleyici, kök günlükleyicinin işleyicilerini ve seviyesini ödünç alır. Kök üzerindeki tek bir basicConfig() çağrısının, uygulamanın başka yerlerindeki her getLogger() çağrısını kurmasının yolu budur.
14.3.1.5. Tembel %-argüman biçimlendirme¶
Şunu yazın:
log.info("processed %d frames in %d ms", count, dt)
Şunu değil:
log.info(f"processed {count} frames in {dt} ms")
% argüman biçimi, günlükleyicinin argümanları seviye filtresi kaydın yayınlanıp yayınlanmayacağına karar verdikten sonra yerleştirmesine olanak tanır. Sıcak bir döngüde filtrelenip atılan bir DEBUG çağrısı, biçim dizesi için hiçbir bedel ödemez. Bir f-string ise her seferinde, çağrı günlükleyiciye ulaşmadan önce, ilk olarak değerlendirilir.
CPython’un yapılandırılmış alanlar için extra= anahtar sözcüğü kamerada desteklenmez; değerleri bunun yerine mesaj argümanları olarak geçirin.
14.3.1.6. İstisnaları günlükleme¶
Bir except bloğunun içinde, Logger.exception() mesajı ERROR seviyesinde günlükler ve mevcut istisnanın geri izlemesini kayda ekler:
try:
frame = csi0.snapshot()
process(frame)
except Exception:
log.exception("frame loop iteration failed")
Geri izleme sys.print_exception() aracılığıyla yakalanır; bir istisna günlüğüne çok satırlı Traceback (most recent call last): bloğunu veren şey budur. Bu, üst düzey istisna işleme için doğru araçtır – yakala, günlükle ve devam et.
14.3.1.7. Birden fazla işleyici¶
En üstte bahsedilen üretim ayrımı – her şey DEBUG seviyesinde bir dosyaya, önemli noktalar WARNING seviyesinde stderr’e – aynı günlükleyiciye bağlı, her biri kendi seviyesi ve biçimlendiricisi olan iki işleyicidir:
import logging
fmt = '%(asctime)s %(levelname)s %(name)s: %(message)s'
file_handler = logging.FileHandler('/sdcard/logs/app.log')
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(fmt))
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.WARNING)
stream_handler.setFormatter(logging.Formatter(fmt))
root = logging.getLogger()
root.setLevel(logging.DEBUG) # admit everything to the filters
root.addHandler(file_handler)
root.addHandler(stream_handler)
Kök günlükleyicinin seviyesi, her kaydın çarptığı ilk filtredir. Onu, herhangi bir işleyicinin görmek istediği en düşük seviyeye ayarlayın – burada DEBUG – böylece hiçbir işleyici günlükleyicinin kendisi tarafından aç bırakılmaz. İşleyici başına seviyeler daha sonra hangi kayıtların gerçekte hangi hedefe yayınlanacağına karar verir.
14.3.1.8. Günlük dosyalarını döndürme¶
Kameranın logging‘inde RotatingFileHandler veya TimedRotatingFileHandler yoktur. Döndürme uygulamanın işidir.
Yöntem, mevcut FileHandler‘ı bilinen bir yerde tutmak, devir ölçütü tetiklendiğinde onu yenisiyle değiştirmek ve tarihli bir yolun doğal dosya sınırını sağlamasına izin vermektir. /sdcard/logs/<year>/<month>/<day>/<hour>.log içine saatlik bir devir için:
import logging
import time
_LOG_FMT = '%(asctime)s %(levelname)s %(name)s: %(message)s'
_current_path = None
_current_handler = None
def _hourly_path(now):
return '/sdcard/logs/{:04d}/{:02d}/{:02d}/{:02d}.log'.format(
now[0], now[1], now[2], now[3])
def rotate_if_needed():
global _current_path, _current_handler
path = _hourly_path(time.localtime())
if path == _current_path:
return
root = logging.getLogger()
if _current_handler is not None:
root.removeHandler(_current_handler)
_current_handler.close()
_current_handler = logging.FileHandler(path)
_current_handler.setFormatter(logging.Formatter(_LOG_FMT))
root.addHandler(_current_handler)
_current_path = path
Ana döngü yinelemesi başına bir kez rotate_if_needed() çağırın; yol kontrolü ucuzdur ve değişim yalnızca saat sınırında gerçekleşir. FileHandler dosyayı açabilmeden önce dizin ağacının var olması gerekir.
14.3.1.9. Güce duyarlı dağıtımlarda boşaltma¶
FileHandler yazmaları, temeldeki dosya nesnesinin Python arabelleğinden geçer. Bir yazma ile bir boşaltma arasında güç kaybı, sondaki kayıtları kaybeder. Pille çalışan veya fişi çekilen dağıtımlar için, kritik kayıtlardan sonra veya bir zamanlayıcıda işleyicinin akışında flush() çağırın.
Kök günlükleyiciye bağlı her işleyiciyi boşaltan küçük bir yardımcı:
import logging
def flush_handlers():
for handler in logging.getLogger().handlers:
if hasattr(handler, 'stream'):
handler.stream.flush()
Uygulamanın kaybetmeyi göze alamayacağı bir kayıttan hemen sonra flush_handlers() çağırın:
log.critical("memory low: restarting")
flush_handlers()
Arka plan güvenliği için, onu günlük tazeliği ile flash aşınması arasında denge kuran hangi ritimle olursa olsun ana döngüden çağırın – saniyede bir kez genellikle yeterlidir. Logger.critical() tek başına bir boşaltmayı tetiklemez.
14.3.1.10. Önyükleme zamanı tanılama¶
Bağlamsız bir saha günlüğü neredeyse işe yaramazdır. Her soğuk önyüklemedeki ilk kayıtlar, hangi kameranın, hangi yapının çalıştığını ve kameranın bu önyüklemeye nasıl ulaştığını belirlemelidir. Aralarında cihaz üzerindeki üç kaynak bunların hepsini kapsar:
omv– OpenMV aygıt yazılımı (firmware) sürümü.os.uname()– MicroPython sürümü, kart adı + MCU ve aygıt yazılımının (firmware) derlendiği kaynağın git etiketi ile derleme tarihi.machine– MCU’nun benzersiz silikon kimliği ve bu önyüklemeyi tetikleyen sıfırlama nedeni.Her bağlama noktasına karşı
os.listdir()– gerçekten devreye giren dosya sistemleri.
Bunların her birini günlüğün ilk kayıtlarına çeken bir yardımcı:
import binascii
import logging
import machine
import omv
import os
log = logging.getLogger(__name__)
_RESET_NAMES = {
machine.PWRON_RESET: "power-on",
machine.HARD_RESET: "hard reset",
machine.WDT_RESET: "watchdog timeout",
machine.DEEPSLEEP_RESET: "wake from deep sleep",
machine.SOFT_RESET: "soft reset",
}
def log_boot_diagnostics():
uname = os.uname()
log.info("machine: %s", uname.machine)
log.info("unique id: %s",
binascii.hexlify(machine.unique_id()).decode())
log.info("firmware: openmv %s, micropython %s",
omv.version_string(), uname.release)
log.info("build: %s", uname.version)
log.info("reset cause: %s",
_RESET_NAMES.get(machine.reset_cause(), "unknown"))
for mount in ('/flash', '/sdcard', '/rom'):
try:
os.listdir(mount)
log.info("mount %s: ok", mount)
except OSError as e:
log.warning("mount %s: %s", mount, e)
Tipik bir günlük şuna benzer bir şeyle açılır:
INFO machine: OPENMV4 with STM32H743
INFO unique id: 002C00543235501020373835
INFO firmware: openmv 5.0.0, micropython 1.28.0
INFO build: v1.28.0-101-gabc1234 on 2026-06-09
INFO reset cause: watchdog timeout
INFO mount /flash: ok
INFO mount /sdcard: ok
INFO mount /rom: ok
Her günlük dosyasına sekiz satır girildiğinde, operatör fiziksel birimi, aygıt yazılımı (firmware) soy ağacını, kameranın neden önyüklendiğini ve hangi depolamanın devreye girdiğini bilir. unique id, MCU’nun fabrikada programlanmış silikon seri numarasıdır; yeniden flash’lamalar ve SD kart değişimleri boyunca aynı kalır. build, görüntünün derlendiği aygıt yazılımı (firmware) ağacının git etiketi ve tarihidir – “bu, tam olarak bu zaman noktasında bu birime sevk edilen ikili dosyadır” diyen tek alan.
14.3.1.11. Hepsini bir araya getirme¶
main.py‘nin başlangıçta bir kez çağırdığı bir yardımcıya ayrıştırılmış, eksiksiz bir üretim günlükleme kurulumu:
import logging
_LOG_FMT = '%(asctime)s %(levelname)s %(name)s: %(message)s'
def setup_logging(log_path):
fh = logging.FileHandler(log_path)
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter(_LOG_FMT))
sh = logging.StreamHandler()
sh.setLevel(logging.WARNING)
sh.setFormatter(logging.Formatter(_LOG_FMT))
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(fh)
root.addHandler(sh)
Ardından main.py‘nin en üstünde:
from app.logging_setup import setup_logging, log_boot_diagnostics
setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()
Uygulamanın başka yerlerindeki her modül sadece şunu yapar:
import logging
log = logging.getLogger(__name__)
ve yapılandırılmış çıktıyı bedavaya alır – tam ayrıntılı dosya, uyarılarla akış, adlandırılmış kayıtlar, zaman damgalı biçimlendirici ve her soğuk başlangıçta belgelenmiş bir önyükleme.