14.3.1. Logging

Produk yang sudah dikirimkan tidak bisa mengirim data ke server dengan print(). print() menulis ke stdout USB, yang hanya ada saat kamera berada di meja pengembang dengan terminal yang terbuka. Di lapangan tidak ada yang membacanya; setiap baris diabaikan begitu saja. Pustaka logging adalah penggantinya -- filter level, tujuan sesuai pilihan aplikasi, dan format yang menjelaskan apa yang terjadi dan kapan.

Modul logging pada kamera adalah port yang dipangkas dari CPython -- model mental yang sama, permukaan yang lebih kecil, beberapa perbedaan yang penting untuk pengaturan produksi.

14.3.1.1. Model mental

Logging dibangun dari empat komponen. Setiap komponen memiliki satu tugas; pemisahan inilah yang memungkinkan satu logger meneruskan ke beberapa tujuan, masing-masing dengan format dan level-nya sendiri:

  • Sebuah Logger adalah yang dipanggil oleh aplikasi. Kode menyebut log.info("frame %d", n); logger adalah objek yang menerima panggilan tersebut. Logger dicari berdasarkan nama dengan logging.getLogger().

  • Sebuah Handler menentukan ke mana sebuah rekaman pergi. Sebuah StreamHandler menulis ke stream (sys.stderr secara default); sebuah FileHandler menambahkan ke file di disk. Sebuah logger dapat memiliki sejumlah handler.

    Catatan

    Pada kamera, sys.stdout dan sys.stderr terhubung ke pipe USB CDC yang sama -- tulisan ke keduanya muncul di terminal yang sama yang dibuka pengembang melalui USB. Sebuah handler yang menulis ke sys.stderr pada praktiknya adalah handler yang menulis ke tempat yang sama dengan print(). Abstraksi handler tetap memberikan Anda filter dan format per-tujuan; hanya saja tidak memberikan Anda saluran fisik yang terpisah.

  • Sebuah Formatter menentukan bagaimana sebuah rekaman dirender menjadi teks. Ia mengambil rekaman dan mengembalikan baris yang akan ditulis. Satu string format per formatter; satu formatter per handler.

  • Sebuah filter level berada pada setiap logger dan setiap handler. Rekaman membawa level (DEBUG / INFO / WARNING / ERROR / CRITICAL). Hanya rekaman pada atau di atas level filter yang bisa lewat.

Pemisahan tersebut penting karena pengaturan produksi tipikal memiliki lebih dari satu tujuan: file di kartu SD yang menyimpan semuanya hingga DEBUG untuk analisis pasca-mortem, dan stream ke USB yang hanya menampilkan WARNING ke atas sehingga pengembang yang terhubung ke kamera melihat hal-hal penting tanpa tenggelam dalam detail. Kode yang sama, dua tujuan, dua filter.

14.3.1.2. Level dan arti masing-masing

Lima level tersebut adalah skala berurutan. Rekaman membawa level sehingga filter pada setiap handler dapat membuang yang tidak dipedulikannya.

  • DEBUG -- pelacakan, penghitung per-bingkai, dump status internal. Level terendah; volume tinggi.

  • INFO -- peristiwa operasional normal. Wi-Fi terhubung, model dimuat, watchdog dimulai, file log baru dirotasi.

  • WARNING -- sesuatu yang tidak terduga tetapi aplikasi menanganinya. Bingkai yang terputus, permintaan jaringan yang dicoba ulang.

  • ERROR -- operasi gagal dan aplikasi tidak dapat menyelesaikannya. File model hilang, penulisan kartu SD ditolak.

  • CRITICAL -- aplikasi tidak dapat melanjutkan sama sekali. Kehabisan memori, mount wajib hilang.

Satu default penting untuk diingat: modul logging kamera memulai setiap logger pada WARNING. Rekaman pada DEBUG dan INFO diam-diam diabaikan kecuali Logger.setLevel() dipanggil -- biasanya sebagai bagian dari panggilan basicConfig() di bawah ini. Gejala umum pertama dari pengaturan logging yang "tidak berfungsi" adalah bahwa aplikasi memancarkan pada INFO dan filter default memakan rekaman tersebut.

Catatan

Level adalah satu-satunya filter yang ditawarkan logging kamera. Tidak ada objek Filter untuk aturan per-rekaman yang lebih kaya; jika level rekaman lolos, rekaman tersebut dipancarkan.

14.3.1.3. basicConfig: mulai cepat

logging.basicConfig() mengonfigurasi root logger dalam satu panggilan. Dua bentuk yang sering muncul:

Pengaturan pengembangan, semuanya ke stderr USB pada INFO

import logging

logging.basicConfig(level=logging.INFO)

Pengaturan produksi, semuanya ke file di kartu SD dengan format bertanda waktu:

import logging

logging.basicConfig(
    filename='/sdcard/logs/app.log',
    level=logging.INFO,
    format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)

Berikan filename= untuk FileHandler atau stream= untuk StreamHandler; keduanya saling eksklusif dalam basicConfig().

String format adalah template bergaya %(field)s. Field yang didukung formatter kamera:

  • %(asctime)s -- cap waktu yang diformat dari time.localtime(). Format default adalah %Y-%m-%d %H:%M:%S; berikan datefmt= untuk mengganti.

  • %(levelname)s -- DEBUG / INFO / WARNING / ERROR / CRITICAL.

  • %(name)s -- nama logger (lihat bagian berikutnya).

  • %(message)s -- pesan yang diformat dari rekaman.

  • %(msecs)d -- fraksi milidetik dari cap waktu rekaman.

Format default jika tidak ada yang diberikan adalah %(levelname)s:%(name)s:%(message)s -- yang cocok untuk pengaturan pengembangan dan tidak memadai untuk log lapangan, di mana cap waktu adalah yang membuat file berguna berminggu-minggu kemudian.

basicConfig() adalah no-op pada panggilan berikutnya kecuali force=True diberikan. Konfigurasikan sekali saat startup; jangan panggil lagi untuk "beralih tujuan" di tengah jalannya program.

Catatan

Modul logging kamera tidak memiliki dictConfig() atau fileConfig(). Konfigurasi selalu bersifat programatik -- satu helper setup_logging() yang dipanggil sekali dari main.py adalah konvensinya.

14.3.1.4. Logger bernama per modul

Kode aplikasi seharusnya tidak memanggil shortcut tingkat modul (logging.info(), logging.warning(), dan sebagainya). Semua itu melalui root logger, dan rekaman log yang dihasilkan membawa nama root -- tidak berguna untuk mengetahui dari mana rekaman tersebut berasal.

Konvensinya adalah satu logger per modul, dinamai sesuai modulnya:

# in app/detector.py
import logging

log = logging.getLogger(__name__)

def detect(frame):
    log.info("detect on %dx%d frame", frame.width(), frame.height())

Setiap rekaman kemudian membawa app.detector di %(name)s dan baris log mengatakan siapa yang memancarkannya.

Modul logging kamera berbeda dari CPython dalam satu hal penting: namespace logger bersifat datar. getLogger('app') dan getLogger('app.detector') adalah logger independen tanpa hubungan induk / anak -- menetapkan level pada app tidak menyebar ke app.detector. Mekanisme yang berfungsi: logger bernama tanpa handler sendiri meminjam handler dan level root logger. Itulah cara satu panggilan basicConfig() pada root mengatur setiap panggilan getLogger() di tempat lain dalam aplikasi.

14.3.1.5. Pemformatan argumen-% yang malas

Tulis:

log.info("processed %d frames in %d ms", count, dt)

Jangan:

log.info(f"processed {count} frames in {dt} ms")

Bentuk argumen % memungkinkan logger menginterpolasi argumen setelah filter level memutuskan apakah akan memancarkan rekaman. Panggilan DEBUG yang difilter dalam loop panas tidak membayar apa pun untuk string formatnya. F-string dievaluasi terlebih dahulu, setiap saat, sebelum panggilan bahkan mencapai logger.

Kata kunci extra= dari CPython untuk field terstruktur tidak didukung pada kamera; berikan nilai sebagai argumen pesan.

14.3.1.6. Logging pengecualian

Di dalam blok except, Logger.exception() mencatat pesan pada level ERROR dan menambahkan traceback pengecualian saat ini ke rekaman:

try:
    frame = csi0.snapshot()
    process(frame)
except Exception:
    log.exception("frame loop iteration failed")

Traceback ditangkap melalui sys.print_exception(), yang memberikan log pengecualian blok multi-baris Traceback (most recent call last):. Ini adalah alat yang tepat untuk penanganan pengecualian tingkat atas -- tangkap, catat, dan lanjutkan.

14.3.1.7. Multiple handler

Pemisahan produksi yang disebutkan di awal -- semuanya ke file pada DEBUG, hal penting ke stderr pada WARNING -- adalah dua handler yang terpasang ke logger yang sama, masing-masing dengan level dan formatter-nya sendiri:

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)

Level root logger adalah filter pertama yang setiap rekaman lalui. Tetapkan ke level terendah yang ingin dilihat oleh handler mana pun -- DEBUG di sini -- sehingga tidak ada handler yang kehabisan karena logger itu sendiri. Level per-handler kemudian memutuskan rekaman mana yang sebenarnya dipancarkan ke tujuan mana.

14.3.1.8. Rotasi file log

Modul logging kamera tidak memiliki RotatingFileHandler atau TimedRotatingFileHandler. Rotasi adalah tugas aplikasi.

Polanya adalah menyimpan FileHandler saat ini di tempat yang diketahui, menukarnya dengan yang baru saat kriteria rollover terpenuhi, dan membiarkan path bertanggal memberikan batas file alami. Untuk rollover per jam ke /sdcard/logs/<year>/<month>/<day>/<hour>.log

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

Panggil rotate_if_needed() sekali per iterasi loop utama; pengecekan path murah dan penukaran hanya terjadi di batas jam. Pohon direktori harus ada sebelum FileHandler dapat membuka file.

14.3.1.9. Flush pada deployment yang sensitif daya

FileHandler menulis melalui buffering Python objek file yang mendasarinya. Kehilangan daya antara penulisan dan flush kehilangan rekaman terakhir. Untuk deployment bertenaga baterai atau yang bisa dimatikan sewaktu-waktu, panggil flush() pada stream handler setelah rekaman kritis, atau pada timer.

Helper kecil yang mem-flush setiap handler yang terpasang ke root logger:

import logging

def flush_handlers():
    for handler in logging.getLogger().handlers:
        if hasattr(handler, 'stream'):
            handler.stream.flush()

Panggil flush_handlers() tepat setelah rekaman yang tidak bisa diabaikan oleh aplikasi:

log.critical("memory low: restarting")
flush_handlers()

Untuk keamanan latar belakang, panggil dari loop utama pada kadens apa pun yang menyeimbangkan kesegaran log terhadap keausan flash -- sekali per detik biasanya sudah cukup. Logger.critical() tidak memicu flush dengan sendirinya.

14.3.1.10. Diagnostik waktu boot

Log lapangan tanpa konteks hampir tidak berguna. Rekaman pertama di setiap cold boot harus mengidentifikasi kamera mana, build apa yang berjalan, dan bagaimana kamera sampai pada boot ini. Tiga sumber on-device bersama-sama mencakup semua itu:

  • omv -- versi firmware OpenMV.

  • os.uname() -- versi MicroPython, nama board + MCU, serta tag git dan tanggal build dari sumber yang digunakan untuk membangun firmware.

  • machine -- ID silikon unik MCU dan penyebab reset yang memicu boot ini.

  • os.listdir() terhadap setiap mount point -- filesystem yang benar-benar berhasil diaktifkan.

Helper yang menarik semuanya ke dalam rekaman pertama log:

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)

Log tipikal dibuka dengan sesuatu seperti:

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

Delapan baris di setiap file log, operator mengetahui unit fisiknya, asal usul firmware, mengapa kamera boot, dan penyimpanan mana yang berhasil aktif. unique id adalah nomor seri silikon yang diprogram di pabrik pada MCU; nilainya sama di seluruh flash ulang dan penggantian kartu SD. build adalah tag git dan tanggal pohon firmware yang digunakan untuk membangun image -- satu-satunya field yang menyatakan "ini adalah tepat binary yang dikirimkan ke unit ini pada titik waktu ini."

14.3.1.11. Menyatukannya

Pengaturan logging produksi yang lengkap, difaktorkan menjadi helper yang dipanggil sekali oleh main.py saat startup:

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)

Kemudian di bagian atas main.py

from app.logging_setup import setup_logging, log_boot_diagnostics

setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()

Setiap modul lain dalam aplikasi hanya melakukan:

import logging

log = logging.getLogger(__name__)

dan mendapatkan output yang dikonfigurasi secara gratis -- file dengan detail lengkap, stream dengan peringatan, rekaman bernama, formatter bertanda waktu, dan boot yang terdokumentasi di setiap cold start.