14.3.1. Bilježenje¶
Gotov proizvod ne može slati dijagnostiku pomoću print(). print() piše na USB stdout, koji postoji samo dok je kamera na razvojnom stolu s otvorenim terminalom. Na terenu to nitko ne čita; svaki redak pada u prazno. Knjižnica logging je zamjena – filtar razine, odredište po izboru aplikacije i format koji govori što se dogodilo i kada.
Modul logging na kameri je pojednostavljeni port CPythonovog – isti mentalni model, manja površina, nekoliko razlika koje su bitne za postavljanje u produkciji.
14.3.1.1. Mentalni model¶
Bilježenje je sastavljeno od četiri dijela. Svaki dio ima jedan posao; to razdvajanje omogućuje da jedan logger razgrana na više odredišta, svako sa svojim formatom i razinom:
Logger je ono što aplikacija poziva. Kod kaže
log.info("frame %d", n); logger je objekt na koji taj poziv pada. Loggeri se traže po imenu pomoćulogging.getLogger().Handler odlučuje kamo zapis ide.
StreamHandlerpiše u stream (sys.stderrpo zadanom);FileHandlerdodaje u datoteku na disku. Logger može imati bilo koji broj handlera.Napomena
Na kameri su
sys.stdoutisys.stderrspojeni na istu USB CDC cijev – pisanja na bilo koji od njih pojavljuju se na istom terminalu koji razvojni programer ima otvoren preko USB-a. Handler koji piše nasys.stderrje, u praksi, handler koji piše na isto mjesto na koje pišeprint(). Apstrakcija handlera ipak vam daje filtriranje i formatiranje po odredištu; samo vam ne daje fizički odvojen kanal.Formatter odlučuje kako se zapis prikazuje kao tekst. Uzima zapis i vraća redak koji se zapisuje. Jedan format string po formatteru; jedan formatter po handleru.
Filtar razine sjedi na svakom loggeru i svakom handleru. Zapisi nose razinu (
DEBUG/INFO/WARNING/ERROR/CRITICAL). Prolaze samo zapisi na razini filtra ili iznad nje.
To razdvajanje je bitno jer tipično produkcijsko postavljanje ima više od jednog odredišta: datoteku na SD kartici koja drži sve do DEBUG za naknadnu analizu, i stream prema USB-u koji prikazuje samo WARNING i gore tako da razvojni programer spojen na kameru vidi najvažnije bez utapanja u detaljima. Isti kod, dva odredišta, dva filtra.
14.3.1.2. Razine i što svaka znači¶
Pet razina čini uređenu ljestvicu. Zapisi nose razinu tako da filtar na svakom handleru može odbaciti one do kojih mu nije stalo.
DEBUG– praćenje, brojači po sličici, ispisi unutarnjeg stanja. Najniža razina; volumen je velik.INFO– normalni operativni događaji. Wi-Fi spojen, model učitan, watchdog pokrenut, nova log datoteka rotirana.WARNING– nešto neočekivano, ali je aplikacija to obradila. Ispuštena sličica, ponovljen mrežni zahtjev.ERROR– operacija nije uspjela i aplikacija je nije mogla dovršiti. Nedostaje datoteka modela, zapis na SD karticu odbijen.CRITICAL– aplikacija uopće ne može nastaviti. Nedostatak memorije, nedostaje obavezna montirana lokacija.
Jedan važan zadani postavak za zapamtiti: kamerin modul logging pokreće svaki logger na WARNING. Zapisi na DEBUG i INFO tiho se odbacuju osim ako se pozove Logger.setLevel() – obično kao dio poziva basicConfig() u nastavku. Čest prvi simptom postavljanja bilježenja koje „ne radi” je da je aplikacija emitirala na INFO a zadani filtar je pojeo zapis.
Napomena
Razina je jedini filtar koji kamerin logging nudi. Nema Filter objekata za bogatija pravila po zapisu; ako razina zapisa prolazi, zapis se emitira.
14.3.1.3. basicConfig: brzi početak¶
logging.basicConfig() konfigurira root logger u jednom pozivu. Najčešće se pojavljuju dva oblika:
Razvojno postavljanje, sve na USB stderr na INFO
import logging
logging.basicConfig(level=logging.INFO)
Produkcijsko postavljanje, sve u datoteku na SD kartici s formatom s vremenskom oznakom:
import logging
logging.basicConfig(
filename='/sdcard/logs/app.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)
Proslijedite ili filename= za FileHandler ili stream= za StreamHandler; ta dva se međusobno isključuju u basicConfig().
Format string je predložak u stilu %(field)s. Polja koja kamerin formatter podržava:
%(asctime)s– vremenska oznaka formatirana iztime.localtime(). Zadani format je%Y-%m-%d %H:%M:%S; proslijeditedatefmt=za nadjačavanje.%(levelname)s–DEBUG/INFO/WARNING/ERROR/CRITICAL.%(name)s– ime loggera (vidi sljedeći odjeljak).%(message)s– formatirana poruka zapisa.%(msecs)d– milisekundni dio vremenske oznake zapisa.
Zadani format ako nijedan nije zadan je %(levelname)s:%(name)s:%(message)s – što je u redu za razvojno postavljanje, a nedovoljno za terenski log, gdje je vremenska oznaka ono što datoteku čini korisnom tjednima kasnije.
basicConfig() ne radi ništa pri sljedećim pozivima osim ako se proslijedi force=True. Konfigurirajte jednom pri pokretanju; ne pozivajte ga ponovno da biste „prebacili odredišta” usred rada.
Napomena
Kamerin logging nema dictConfig() ni fileConfig(). Konfiguracija je uvijek programska – jedan pomoćnik setup_logging() pozvan jednom iz main.py je konvencija.
14.3.1.4. Imenovani loggeri po modulu¶
Aplikacijski kod ne bi trebao pozivati prečace na razini modula (logging.info(), logging.warning() i tako dalje). Svi oni prolaze kroz root logger, a rezultirajući log zapisi nose ime root – beskorisno za utvrđivanje odakle zapis dolazi.
Konvencija je jedan logger po modulu, nazvan po modulu:
# in app/detector.py
import logging
log = logging.getLogger(__name__)
def detect(frame):
log.info("detect on %dx%d frame", frame.width(), frame.height())
Svaki zapis tada nosi app.detector u %(name)s i log redak govori tko ga je emitirao.
Kamerin logging razlikuje se od CPythona na jedan važan način: imenski prostor loggera je plosnat. getLogger('app') i getLogger('app.detector') su neovisni loggeri bez odnosa roditelj / dijete – postavljanje razine na app ne propagira na app.detector. Mehanizam koji radi: imenovani logger bez vlastitih handlera posuđuje handlere i razinu root loggera. Tako jedan poziv basicConfig() na root postavlja svaki poziv getLogger() drugdje u aplikaciji.
14.3.1.5. Lijeno formatiranje %-argumenata¶
Pišite:
log.info("processed %d frames in %d ms", count, dt)
Ne:
log.info(f"processed {count} frames in {dt} ms")
Oblik s %-argumentima omogućuje loggeru da interpolira argumente nakon što je filtar razine odlučio hoće li emitirati zapis. Odfiltriran poziv DEBUG u vrućoj petlji ne plaća ništa za svoj format string. F-string se evaluira prvi, svaki put, prije nego što poziv uopće dođe do loggera.
CPythonova ključna riječ extra= za strukturirana polja nije podržana na kameri; umjesto toga proslijedite vrijednosti kao argumente poruke.
14.3.1.6. Bilježenje iznimaka¶
Unutar except bloka, Logger.exception() bilježi poruku na razini ERROR i dodaje traceback trenutne iznimke u zapis:
try:
frame = csi0.snapshot()
process(frame)
except Exception:
log.exception("frame loop iteration failed")
Traceback se hvata pomoću sys.print_exception(), što je ono što log iznimke daje višeretčani Traceback (most recent call last): blok. Ovo je pravi alat za obradu iznimaka na najvišoj razini – uhvati, zabilježi i nastavi.
14.3.1.7. Više handlera¶
Produkcijska podjela spomenuta na vrhu – sve u datoteku na DEBUG, najvažnije na stderr na WARNING – su dva handlera priključena na isti logger, svaki sa svojom razinom i formatterom:
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)
Razina root loggera je prvi filtar koji svaki zapis pogađa. Postavite ju na najnižu razinu koju bilo koji handler želi vidjeti – ovdje DEBUG – tako da nijedan handler ne bude izgladnjen samim loggerom. Razine po handleru tada odlučuju koji se zapisi zapravo emitiraju na koje odredište.
14.3.1.8. Rotacija log datoteka¶
Kamerin logging nema RotatingFileHandler ni TimedRotatingFileHandler. Rotacija je posao aplikacije.
Obrazac je zadržati trenutni FileHandler na poznatom mjestu, zamijeniti ga novim kad se pokrene kriterij rotacije i pustiti da datirana putanja pruži prirodnu granicu datoteke. Za satnu rotaciju u /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
Pozivajte rotate_if_needed() jednom po iteraciji glavne petlje; provjera putanje je jeftina i zamjena se događa samo na granici sata. Stablo direktorija mora postojati prije nego što FileHandler može otvoriti datoteku.
14.3.1.9. Pražnjenje međuspremnika na implementacijama osjetljivima na napajanje¶
Pisanja FileHandler prolaze kroz Python međuspremnik podležećeg objekta datoteke. Gubitak napajanja između pisanja i pražnjenja gubi posljednje zapise. Za implementacije na bateriju ili one gdje se napajanje naglo prekida, pozovite flush() na streamu handlera nakon kritičnih zapisa, ili na mjeraču vremena.
Mali pomoćnik koji prazni svaki handler priključen na root logger:
import logging
def flush_handlers():
for handler in logging.getLogger().handlers:
if hasattr(handler, 'stream'):
handler.stream.flush()
Pozovite flush_handlers() odmah nakon zapisa koji si aplikacija ne može priuštiti izgubiti:
log.critical("memory low: restarting")
flush_handlers()
Za pozadinsku sigurnost, pozivajte ga iz glavne petlje u kadenci koja uravnotežuje svježinu loga naspram trošenja flash memorije – jednom u sekundi je obično dovoljno. Logger.critical() sam po sebi ne pokreće pražnjenje.
14.3.1.10. Dijagnostika pri pokretanju¶
Terenski log bez konteksta gotovo je beskoristan. Prvi zapisi pri svakom hladnom pokretanju trebali bi identificirati koja kamera, koji build se izvodi i kako je kamera došla do ovog pokretanja. Tri izvora na uređaju zajedno pokrivaju sve to:
omv– verzija OpenMV ugrađenog programa (firmware).os.uname()– verzija MicroPythona, ime ploče + MCU, te git oznaka i datum builda izvornog koda iz kojeg je ugrađeni program (firmware) izgrađen.machine– jedinstveni silicijski ID MCU-a i uzrok resetiranja koji je pokrenuo ovo pokretanje.os.listdir()nad svakom točkom montiranja – datotečni sustavi koji su zapravo došli do izvođenja.
Pomoćnik koji povlači svaki od tih podataka u prve zapise loga:
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)
Tipičan log otvara se nečim poput:
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
Osam redaka u svaku log datoteku, operater zna fizičku jedinicu, lozu ugrađenog programa (firmware), zašto se kamera pokrenula i koje je pohrana došlo do izvođenja. unique id je tvornički programiran silicijski serijski broj MCU-a; isti je kroz ponovna prebacivanja firmwarea i kroz zamjene SD kartica. build je git oznaka i datum stabla firmwarea iz kojeg je slika izgrađena – jedino polje koje kaže „ovo je točno binarni kod koji je isporučen ovoj jedinici u ovom trenutku.”
14.3.1.11. Spajanje svega¶
Cjelovito produkcijsko postavljanje bilježenja, izdvojeno u pomoćnik koji main.py poziva jednom pri pokretanju:
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)
Zatim na vrhu main.py
from app.logging_setup import setup_logging, log_boot_diagnostics
setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()
Svaki modul drugdje u aplikaciji samo radi:
import logging
log = logging.getLogger(__name__)
i besplatno dobiva konfigurirani izlaz – datoteka s punim detaljima, stream s upozorenjima, imenovani zapisi, formatter s vremenskom oznakom i dokumentirano pokretanje pri svakom hladnom startu.