Repository navigation
refactor(logs): homogeneous logging with LOG_LEVEL debug mode - #417
Open
MateoLostanlen wants to merge 19 commits into
Open
MateoLostanlen wants to merge 19 commits into
MateoLostanlen wants to merge 19 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #417 +/- ##
===========================================
+ Coverage 80.51% 81.05% +0.53%
===========================================
Files 6 7 +1
Lines 847 871 +24
===========================================
+ Hits 682 706 +24
Misses 165 165
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
fe51
reviewed
Oct 23, 2026
fe51
left a comment
Member
There was a problem hiding this comment.
Reviewed — LGTM overall. The logging refactor is clean, well tested, and easy to roll back (revert the merge commit).
One thing worth fixing before or right after merge:
- Healthcheck vs. active streams (
pyroengine/core.py:194):inference_loopreturns early while a stream is active, and the heartbeat is only touched on a successful capture (or during the night sleep). Previously the healthcheck was agreponengine.log, which never went stale. With the new 10 min freshness check, a stream longer than 10 min makes the engine report unhealthy even though it is intentionally paused. This only matters if something restarts unhealthy containers (e.g. autoheal), but a one-lineself._write_heartbeat()before that earlyreturnwould avoid it.
Minor, non-blocking:
- The secrets redaction filter doesn't cover exception tracebacks (
exc_text), so a URL with credentials inside arequestserror could still be logged by the newlogger.exceptioncalls. - The
_SENSITIVE_KVregex is broad (u=,p=,user=,auth=), so values likep=0.35get masked in unrelated log lines. getMessage()in the filter is unguarded; a malformedlogger.info("x %s %s", a)from any logger would raise instead of being swallowed by logging.
Libraries no longer configure the root logger: basicConfig is called only from the two entrypoints, with the same format in both services. LOG_LEVEL is now plumbed through docker-compose and documented in .env.example.
Per-pose captures, raw predictions and no-detection results move to DEBUG. Each pass now ends with one INFO summary (analyzed/failed/positive/max confidence/duration). Detections and alerts stay at INFO. Startup retries are warnings instead of errors, unexpected failures use logger.exception, and every camera line carries a [cam_id] prefix. Classifier no longer mutates the module logger level from its constructor.
One INFO line per patrol cycle, per-pose moves and captures move to DEBUG. The static loop stays silent while healthy and reports recovery instead of repeating a warning every 30s during a skip window. ffmpeg stderr, FPS meter, mock no-ops and unsupported Linovision features drop to DEBUG. RTSP URLs and ffmpeg commands are redacted before logging, using a shared helper extracted from the RTSP adapter.
Wiring LOG_LEVEL to the engine made the healthcheck grep for strings that are only emitted at INFO, so LOG_LEVEL=WARNING would mark a healthy engine unhealthy. The main loop now refreshes data/heartbeat and the healthcheck checks its freshness instead. The night sleep is chunked so it stays fresh. Third-party loggers are also clamped to the configured level: pinning them to WARNING let their warnings through an ERROR root logger.
Refresh the heartbeat per pose and during the end-of-loop sleep so a slow round or a large --period cannot exceed the healthcheck window. The heartbeat path is now its own --heartbeat-file option instead of being derived from --cache, so it stays aligned with the healthcheck.
Uvicorn keeps its own handlers with propagate=False, so its startup and access lines kept a timestamp-less format of their own. They now go through the root handler like every other line.
Splitting on the first @ leaked the tail of any password containing one: rtsp://admin:p@ssword@host became rtsp://***:***@ssword@host. It also mangled credential-free URLs whose path contains @, dropping the host. Parse the URL and mask the netloc userinfo instead, and leave anything that is not a scheme://host URL untouched so the helper stays safe to map over a whole ffmpeg command line.
Uvicorn's dictConfig pins an explicit INFO level on uvicorn and uvicorn.error, so its startup lines stayed visible at LOG_LEVEL=ERROR. Reset them to NOTSET so they inherit the configured level.
verbose was accepted but ignored, so library callers passing verbose=False started seeing detection and model setup lines. Gate those logs per instance, downgrading them to DEBUG, which restores the observable behaviour without the old global logger.setLevel side effect.
One redaction module covers userinfo, credential query params and MediaMTX streamid forms; a handler filter scrubs relayed lines (ffmpeg stderr) too. rest.py and url.py delegate to it instead of keeping weaker copies.
Written only on successful capture, night sleep or responsive autofocus, and deleted at startup, so a blind or crash-looping engine goes unhealthy instead of keeping a stale file fresh. Healthcheck window 10 min, start_period covers the cold-boot model download. The summary counts the predictor's ongoing verdict so hysteresis-kept events no longer read as a quiet round. Drop the dead tee/engine.log wrapper.
MateoLostanlen
force-pushed
the
refactor/homogeneous-logging
branch
from
October 23, 2026 14:47
962363a to
93e0395
Compare
Member
Author
|
Hi @fe51 Thanks for the review! Rebased on develop and addressed your points:
Each case has a test. |
The heartbeat now tracks liveness (every loop pass) instead of image flow, so offline cameras do not cause restart loops.
Open
1 task
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces #392, closed by mistake (GitHub cannot reopen a PR once its branch was deleted).
What changes
pyroengine,pyro_camera_apiandpyro-predictor: only the two entrypoints configure logging, both with the same format, and verbosity is driven by a newLOG_LEVELenv var wired through docker-compose. Library modules no longer calllogging.basicConfig(force=True)at import. Uvicorn lines are routed through the same formatter too, so every line in both containers shares one format.logger.exception, a known down camera is reported once instead of every 30s, and every camera scoped line carries a[cam_id]prefix.src/get their logging output back (they relied on thebasicConfigthat libraries no longer call), a frame dropped during the patrol settle window is no longer counted as a capture failure, and thepyro_camera_apitest suite now runs in CI.Examples
Output below was captured by running the same script on
developand on this branch.One quiet round, 4 poses, nothing detected (every 30s)
Before, 18 lines:
After, 1 line:
The prediction array was printed twice per pose, once by
vision.pyand once byengine.py.Run focus finderprinted on every round even thoughfocus_finder()only acts once an hour.A real detection stays visible
Format now carries the logger name
The camera API used a different format in the same deployment. Both now share one format, and engine logs go to stdout explicitly instead of stderr.
Uvicorn kept its own handlers with
propagate=False, so its lines had no timestamp at all:The per request access line is now DEBUG only, since the engine polls the capture endpoints every few seconds. At
LOG_LEVEL=DEBUGit comes back, in the shared format:Levels
Patrol cycle, 4 poses every 30s
Credentials
Nothing is lost at LOG_LEVEL=DEBUG
Deployment note
The engine healthcheck no longer greps
engine.log. It now checks that the heartbeat file (option--heartbeat-file, defaultdata/heartbeat) was touched in the last 10 minutes. The file is refreshed on every loop pass, through the night sleep and during autofocus on a responsive camera. When it is older than 10 min the healthcheck kills every process but PID 1 (tini, viainit: true) sorestart: alwaysbrings the engine back. No file yet means the engine is still starting, which never triggers a kill.start_periodmoves from 20s to 300s to cover the model download and the first loop.