14.3.1. Jurnalizare¶
Un produs livrat nu poate raporta prin print(). print() scrie pe ieșirea standard USB, care există doar atunci când camera se află pe masa de lucru a unui dezvoltator, cu un terminal deschis. Pe teren, nimeni nu o citește; fiecare linie este pierdută. Biblioteca logging este înlocuitorul – un filtru de nivel, o destinație aleasă de aplicație și un format care spune ce s-a întâmplat și când.
Modulul logging de pe cameră este un port simplificat al celui din CPython – același model mental, o suprafață mai mică și câteva diferențe care contează pentru configurarea în producție.
14.3.1.1. Modelul mental¶
Jurnalizarea este construită din patru componente. Fiecare componentă are o singură sarcină; tocmai această separare permite unui singur logger să distribuie către mai multe destinații, fiecare cu propriul format și nivel:
Un Logger este ceea ce apelează aplicația. Codul spune
log.info("frame %d", n); logger-ul este obiectul pe care aterizează acel apel. Logger-ele sunt căutate după nume culogging.getLogger().Un Handler decide unde ajunge o înregistrare. Un
StreamHandlerscrie într-un flux (sys.stderrîn mod implicit); unFileHandleradaugă într-un fișier de pe disc. Un logger poate avea oricâte handlere.Notă
Pe cameră,
sys.stdoutșisys.stderrsunt conectate la aceeași conductă USB CDC – scrierile către oricare dintre ele apar pe același terminal pe care un dezvoltator îl are deschis prin USB. Un handler care scrie însys.stderreste, în practică, un handler care scrie în același loc în care scrieprint(). Abstracția handler-ului îți oferă în continuare filtrare și formatare per destinație; pur și simplu nu îți oferă un canal fizic separat.Un Formatter decide cum este redată o înregistrare ca text. El preia o înregistrare și returnează linia care va fi scrisă. Un singur șir de format per formatter; un singur formatter per handler.
Un filtru de nivel se află pe fiecare logger și pe fiecare handler. Înregistrările poartă un nivel (
DEBUG/INFO/WARNING/ERROR/CRITICAL). Doar înregistrările de la nivelul filtrului în sus trec mai departe.
Acea separare contează deoarece o configurație tipică de producție are mai mult de o destinație: un fișier pe cardul SD care păstrează totul până la DEBUG pentru analiza ulterioară și un flux către USB care afișează doar WARNING și mai grav, astfel încât un dezvoltator conectat la cameră să vadă punctele importante fără a se îneca în detalii. Același cod, două destinații, două filtre.
14.3.1.2. Nivelurile și ce înseamnă fiecare¶
Cele cinci niveluri formează o scală ordonată. Înregistrările poartă un nivel astfel încât filtrul de pe fiecare handler să poată elimina pe cele care nu îl interesează.
DEBUG– urmărire, contoare per cadru, dump-uri ale stării interne. Cel mai jos nivel; volumul este ridicat.INFO– evenimente operaționale normale. Wi-Fi conectat, un model încărcat, watchdog-ul pornit, un nou fișier de jurnal rotit în uz.WARNING– ceva neașteptat, dar aplicația l-a gestionat. Un cadru pierdut, o cerere de rețea reîncercată.ERROR– o operație a eșuat și aplicația nu a putut-o finaliza. Un fișier de model lipsă, o scriere pe cardul SD refuzată.CRITICAL– aplicația nu poate continua deloc. Memorie epuizată, montare obligatorie lipsă.
O valoare implicită importantă de reținut: modulul logging al camerei pornește fiecare logger la WARNING. Înregistrările de la DEBUG și INFO sunt eliminate în tăcere dacă nu se apelează Logger.setLevel() – de obicei ca parte a apelului basicConfig() de mai jos. Un prim simptom frecvent al unei configurări de jurnalizare care „nu funcționează” este faptul că aplicația a emis la INFO, iar filtrul implicit a înghițit înregistrarea.
Notă
Nivelul este singurul filtru pe care îl oferă modulul logging al camerei. Nu există obiecte Filter pentru reguli mai bogate per înregistrare; dacă nivelul unei înregistrări trece, aceasta este emisă.
14.3.1.3. basicConfig: pornirea rapidă¶
logging.basicConfig() configurează logger-ul rădăcină într-un singur apel. Două forme apar cel mai des:
O configurație de dezvoltare, totul către stderr USB la INFO
import logging
logging.basicConfig(level=logging.INFO)
O configurație de producție, totul către un fișier de pe cardul SD cu un format cu marcaj temporal:
import logging
logging.basicConfig(
filename='/sdcard/logs/app.log',
level=logging.INFO,
format='%(asctime)s %(levelname)s %(name)s: %(message)s',
)
Transmite fie filename= pentru un FileHandler, fie stream= pentru un StreamHandler; cele două se exclud reciproc în basicConfig().
Șirul de format este un șablon în stil %(field)s. Câmpurile pe care le acceptă formatter-ul camerei:
%(asctime)s– marcaj temporal formatat dintime.localtime(). Formatul implicit este%Y-%m-%d %H:%M:%S; transmitedatefmt=pentru a-l suprascrie.%(levelname)s–DEBUG/INFO/WARNING/ERROR/CRITICAL.%(name)s– numele logger-ului (vezi secțiunea următoare).%(message)s– mesajul formatat al înregistrării.%(msecs)d– fracțiunea de milisecunde a marcajului temporal al înregistrării.
Formatul implicit dacă nu este furnizat niciunul este %(levelname)s:%(name)s:%(message)s – ceea ce este în regulă pentru configurația de dezvoltare și inadecvat pentru un jurnal de teren, unde marcajul temporal este cel care face fișierul util săptămâni mai târziu.
basicConfig() nu face nimic la apelurile ulterioare dacă nu se transmite force=True. Configurează o singură dată la pornire; nu îl apela din nou pentru a „comuta destinațiile” în timpul execuției.
Notă
Modulul logging al camerei nu are dictConfig() sau fileConfig(). Configurarea este întotdeauna programatică – un singur ajutor setup_logging() apelat o singură dată din main.py este convenția.
14.3.1.4. Loggere denumite per modul¶
Codul aplicației nu ar trebui să apeleze comenzile rapide la nivel de modul (logging.info(), logging.warning() și așa mai departe). Toate acestea trec prin logger-ul rădăcină, iar înregistrările de jurnal rezultate poartă numele root – inutil pentru a spune de unde a provenit înregistrarea.
Convenția este un logger per modul, denumit după modul:
# in app/detector.py
import logging
log = logging.getLogger(__name__)
def detect(frame):
log.info("detect on %dx%d frame", frame.width(), frame.height())
Fiecare înregistrare poartă apoi app.detector în %(name)s, iar linia de jurnal spune cine a emis-o.
Modulul logging al camerei diferă de CPython într-un mod important: spațiul de nume al logger-elor este plat. getLogger('app') și getLogger('app.detector') sunt loggere independente, fără relație părinte / copil – setarea unui nivel pe app nu se propagă la app.detector. Mecanismul care funcționează: un logger denumit fără handlere proprii împrumută handlerele și nivelul logger-ului rădăcină. Astfel un singur apel basicConfig() pe rădăcină configurează fiecare apel getLogger() din altă parte a aplicației.
14.3.1.5. Formatarea leneșă cu argumente %¶
Scrie:
log.info("processed %d frames in %d ms", count, dt)
Nu:
log.info(f"processed {count} frames in {dt} ms")
Forma cu argument % îi permite logger-ului să interpoleze argumentele după ce filtrul de nivel a decis dacă să emită înregistrarea. Un apel DEBUG filtrat dintr-o buclă intensă nu plătește nimic pentru șirul său de format. Un f-string se evaluează mai întâi, de fiecare dată, înainte ca apelul să ajungă măcar la logger.
Cuvântul cheie extra= din CPython pentru câmpuri structurate nu este acceptat pe cameră; transmite valorile ca argumente ale mesajului în schimb.
14.3.1.6. Jurnalizarea excepțiilor¶
În interiorul unui bloc except, Logger.exception() jurnalizează mesajul la nivelul ERROR și adaugă la înregistrare urmărirea stivei excepției curente:
try:
frame = csi0.snapshot()
process(frame)
except Exception:
log.exception("frame loop iteration failed")
Urmărirea stivei este capturată prin sys.print_exception(), ceea ce conferă unui jurnal de excepții blocul său pe mai multe linii Traceback (most recent call last):. Acesta este instrumentul potrivit pentru gestionarea excepțiilor la nivel superior – prinde, jurnalizează și continuă.
14.3.1.7. Mai multe handlere¶
Împărțirea pentru producție menționată la început – totul către un fișier la DEBUG, punctele importante către stderr la WARNING – înseamnă două handlere atașate aceluiași logger, fiecare cu propriul nivel și formatter:
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)
Nivelul logger-ului rădăcină este primul filtru pe care îl întâlnește fiecare înregistrare. Setează-l la cel mai jos nivel pe care vrea să-l vadă oricare handler – DEBUG aici – astfel încât niciun handler să nu fie privat de înregistrări de însuși logger-ul. Nivelurile per handler decid apoi care înregistrări sunt efectiv emise către care destinație.
14.3.1.8. Rotirea fișierelor de jurnal¶
Modulul logging al camerei nu are RotatingFileHandler sau TimedRotatingFileHandler. Rotirea este sarcina aplicației.
Modelul este să păstrezi FileHandler-ul curent într-un loc cunoscut, să-l înlocuiești cu unul nou când se declanșează criteriul de rotire și să lași o cale cu dată să furnizeze granița naturală a fișierului. Pentru o rotire orară în /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
Apelează rotate_if_needed() o dată per iterație a buclei principale; verificarea căii este ieftină, iar înlocuirea se întâmplă doar la granița orei. Arborele de directoare trebuie să existe înainte ca FileHandler să poată deschide fișierul.
14.3.1.9. Golirea bufferului în implementări sensibile la alimentare¶
Scrierile FileHandler trec prin buffering-ul Python al obiectului fișier subiacent. O pierdere de alimentare între o scriere și o golire pierde înregistrările finale. Pentru implementări alimentate de baterie sau de tip scoate-din-priză, apelează flush() pe fluxul handler-ului după înregistrările critice, sau pe un temporizator.
Un mic ajutor care golește fiecare handler atașat logger-ului rădăcină:
import logging
def flush_handlers():
for handler in logging.getLogger().handlers:
if hasattr(handler, 'stream'):
handler.stream.flush()
Apelează flush_handlers() imediat după o înregistrare pe care aplicația nu-și poate permite să o piardă:
log.critical("memory low: restarting")
flush_handlers()
Pentru siguranță în fundal, apelează-l din bucla principală la orice cadență care echilibrează prospețimea jurnalului cu uzura memoriei flash – o dată pe secundă este de obicei suficient. Logger.critical() nu declanșează o golire prin sine.
14.3.1.10. Diagnostice la pornire¶
Un jurnal de teren fără context este aproape inutil. Primele înregistrări la fiecare pornire la rece ar trebui să identifice care cameră, ce build rulează și cum a ajuns camera la această pornire. Trei surse de pe dispozitiv acoperă împreună toate acestea:
omv– versiunea de firmware OpenMV.os.uname()– versiunea MicroPython, numele plăcii + MCU, precum și eticheta git și data de build a sursei din care a fost compilat firmware-ul.machine– ID-ul unic de siliciu al MCU-ului și cauza resetării care a declanșat această pornire.os.listdir()pentru fiecare punct de montare – sistemele de fișiere care au pornit efectiv.
Un ajutor care extrage fiecare dintre acestea în primele înregistrări ale jurnalului:
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)
Un jurnal tipic se deschide cu ceva de genul:
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
La opt linii în fiecare fișier de jurnal, operatorul cunoaște unitatea fizică, descendența firmware-ului, motivul pentru care a pornit camera și ce stocare a pornit. unique id este numărul de serie de siliciu programat din fabrică al MCU-ului; este același după reflash-uri și după schimbări ale cardului SD. build este eticheta git și data arborelui de firmware din care a fost compilată imaginea – singurul câmp care spune „acesta este exact binarul care a fost livrat acestei unități în acest moment.”
14.3.1.11. Punând totul cap la cap¶
O configurație completă de jurnalizare în producție, factorizată într-un ajutor pe care main.py îl apelează o singură dată la pornire:
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)
Apoi în partea de sus a main.py
from app.logging_setup import setup_logging, log_boot_diagnostics
setup_logging('/sdcard/logs/app.log')
log_boot_diagnostics()
Fiecare modul din altă parte a aplicației face pur și simplu:
import logging
log = logging.getLogger(__name__)
și obține gratuit ieșirea configurată – fișier cu detalii complete, flux cu avertismente, înregistrări denumite, formatter cu marcaj temporal și o pornire documentată la fiecare pornire la rece.