Skip to content

Commit 96a2d96

Browse files
authored
Server: Back off reading logs in the log tab on error (#403)
* Server: Back off reading logs in the log tab, if the log file cannot be found. * multiproclogging: Add type annotation for mypy error
1 parent 7f830e2 commit 96a2d96

4 files changed

Lines changed: 90 additions & 32 deletions

File tree

python/PiFinder/main.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -868,25 +868,36 @@ def main(
868868

869869

870870
if __name__ == "__main__":
871+
import sys
872+
873+
debug_no_file_logs = "--debug-no-file-logs" in sys.argv
874+
if debug_no_file_logs:
875+
os.environ["PIFINDER_DEBUG_NO_FILE_LOGS"] = "1"
876+
871877
print("Bootstrap logging configuration ...")
872878
logging.basicConfig(format="%(asctime)s BASIC %(name)s: %(levelname)s %(message)s")
873879
rlogger = logging.getLogger()
874-
rlogger.setLevel(logging.INFO)
875-
log_path = utils.data_dir / "pifinder.log"
876-
try:
877-
log_helper = MultiprocLogging(
878-
Path("pifinder_logconf.json"),
879-
log_path,
880-
)
880+
rlogger.setLevel(logging.DEBUG if debug_no_file_logs else logging.INFO)
881+
882+
if debug_no_file_logs:
883+
log_helper = MultiprocLogging(Path("pifinder_logconf.json"), console_only=True)
881884
MultiprocLogging.configurer(log_helper.get_queue())
882-
except FileNotFoundError:
883-
rlogger.warning(
884-
"Cannot find log configuration file, proceeding with basic configuration."
885-
)
886-
rlogger.warning("Logs will not be stored on disk, unless you use --log")
887-
logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING)
888-
logging.getLogger("tetra3.Tetra3").setLevel(logging.WARNING)
889-
logging.getLogger("picamera2.picamera2").setLevel(logging.WARNING)
885+
else:
886+
log_path = utils.data_dir / "pifinder.log"
887+
try:
888+
log_helper = MultiprocLogging(
889+
Path("pifinder_logconf.json"),
890+
log_path,
891+
)
892+
MultiprocLogging.configurer(log_helper.get_queue())
893+
except FileNotFoundError:
894+
rlogger.warning(
895+
"Cannot find log configuration file, proceeding with basic configuration."
896+
)
897+
rlogger.warning("Logs will not be stored on disk, unless you use --log")
898+
logging.getLogger("PIL.PngImagePlugin").setLevel(logging.WARNING)
899+
logging.getLogger("tetra3.Tetra3").setLevel(logging.WARNING)
900+
logging.getLogger("picamera2.picamera2").setLevel(logging.WARNING)
890901

891902
rlogger.info("Starting PiFinder ...")
892903
parser = argparse.ArgumentParser(description="eFinder")
@@ -948,6 +959,11 @@ def main(
948959
"-x", "--verbose", help="Set logging to debug mode", action="store_true"
949960
)
950961
parser.add_argument("-l", "--log", help="Log to file", action="store_true")
962+
parser.add_argument(
963+
"--debug-no-file-logs",
964+
help="Debug: log everything at DEBUG level to console only, bypassing log configuration and file output",
965+
action="store_true",
966+
)
951967
parser.add_argument(
952968
"--lang",
953969
help="Force user interface language (iso2 code). Changes configuration",

python/PiFinder/multiproclogging.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,18 @@ def __init__(
6767
log_conf: Optional[Path] = None,
6868
out_file: Optional[Path] = None,
6969
formatter: str = "%(asctime)s %(processName)s-%(name)s:%(levelname)s:%(message)s",
70+
console_only: bool = False,
7071
):
7172
self._queues: List[Queue] = []
7273
self._log_conf_file = log_conf
7374
self._log_output_file = out_file
7475
self._formatter = formatter
76+
self._console_only = console_only
7577
self._proc: Optional[Process] = None
7678

7779
self.apply_config()
80+
if console_only:
81+
logging.getLogger().setLevel(logging.DEBUG)
7882

7983
def apply_config(self):
8084
if self._log_conf_file is not None:
@@ -94,7 +98,7 @@ def start(self, initial_queue: Optional[Queue] = None):
9498
self._proc = Process(
9599
target=self._run_sink,
96100
args=(
97-
self._log_output_file,
101+
None if self._console_only else self._log_output_file,
98102
self._queues,
99103
),
100104
)
@@ -109,7 +113,7 @@ def join(self):
109113
self._queues[0].put(None)
110114
self._proc.join()
111115

112-
def _run_sink(self, output: Path, queues: List[Queue]):
116+
def _run_sink(self, output: Optional[Path], queues: List[Queue]):
113117
"""
114118
This is the process that consumes every log message (sink)
115119
@@ -124,15 +128,22 @@ def _run_sink(self, output: Path, queues: List[Queue]):
124128
for hdlr in hdlrs:
125129
rLogger.removeHandler(hdlr)
126130

127-
# Set maxBytes to 50MB (50 * 1024 * 1024 bytes) and keep 5 backup files
128-
h = logging.handlers.RotatingFileHandler(
129-
output, maxBytes=50 * 1024 * 1024, backupCount=5, encoding="utf-8"
130-
)
131+
if output is None:
132+
import sys
133+
134+
h: logging.Handler = logging.StreamHandler(sys.stderr)
135+
rLogger.setLevel(logging.DEBUG)
136+
rLogger.warning("Starting logging process (console only, no file output)")
137+
else:
138+
# Set maxBytes to 50MB (50 * 1024 * 1024 bytes) and keep 5 backup files
139+
h = logging.handlers.RotatingFileHandler(
140+
output, maxBytes=50 * 1024 * 1024, backupCount=5, encoding="utf-8"
141+
)
142+
rLogger.warning("Starting logging process")
143+
rLogger.warning("Logging to %s", output)
131144
f = logging.Formatter(self._formatter)
132145
h.setFormatter(f)
133146
rLogger.addHandler(h)
134-
rLogger.warning("Starting logging process")
135-
rLogger.warning("Logging to %s", output)
136147

137148
# import logging_tree
138149
# logging_tree.printout()
@@ -172,6 +183,8 @@ def configurer(queue: Queue):
172183
This method needs to be called once in each process, so that log records get forwarded to the single process writing
173184
log messages.
174185
"""
186+
import os
187+
175188
assert queue is not None, "You passed a None to configurer! You cannot do that"
176189
assert isinstance(
177190
queue, multiprocessing.queues.Queue
@@ -182,8 +195,11 @@ def configurer(queue: Queue):
182195
config = json5.load(logconf)
183196
logging.config.dictConfig(config)
184197

185-
h = logging.handlers.QueueHandler(queue)
186198
root = logging.getLogger()
199+
if os.environ.get("PIFINDER_DEBUG_NO_FILE_LOGS"):
200+
root.setLevel(logging.DEBUG)
201+
202+
h = logging.handlers.QueueHandler(queue)
187203
root.addHandler(h)
188204

189205
def set_log_conf_file(self, config: Path) -> None:

python/PiFinder/server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -802,8 +802,8 @@ def stream_logs():
802802
else:
803803
return {"logs": [], "position": position}
804804
except FileNotFoundError:
805-
logger.error(f"Log file not found: {log_file}")
806-
return {"logs": [], "position": 0}
805+
logger.warning(f"Log file not found: {log_file}")
806+
return {"logs": [], "position": 0, "file_not_found": True}
807807

808808
except Exception as e:
809809
logger.error(f"Error streaming logs: {e}")

python/views/logs.tpl

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -164,30 +164,54 @@ const LINE_HEIGHT = 20;
164164
let updateInterval;
165165
let lastLine = '';
166166
167+
// Backoff state for when the log file is not yet available.
168+
// This page intentionally does NOT fetch the home route, so iwgetid is never triggered here.
169+
const MIN_POLL_INTERVAL = 1000;
170+
const MAX_POLL_INTERVAL = 10000;
171+
let currentPollInterval = MIN_POLL_INTERVAL;
172+
173+
function scheduleFetch() {
174+
clearInterval(updateInterval);
175+
updateInterval = setInterval(fetchLogs, currentPollInterval);
176+
}
177+
167178
function fetchLogs() {
168179
if (isPaused) return;
169-
180+
170181
fetch(`/logs/stream?position=${currentPosition}`)
171182
.then(response => response.json())
172183
.then(data => {
184+
if (data.file_not_found) {
185+
// Back off exponentially up to MAX_POLL_INTERVAL
186+
currentPollInterval = Math.min(currentPollInterval * 2, MAX_POLL_INTERVAL);
187+
scheduleFetch();
188+
return;
189+
}
190+
191+
// Reset backoff on a successful response
192+
if (currentPollInterval !== MIN_POLL_INTERVAL) {
193+
currentPollInterval = MIN_POLL_INTERVAL;
194+
scheduleFetch();
195+
}
196+
173197
if (!data.logs || data.logs.length === 0) return;
174-
198+
175199
currentPosition = data.position;
176200
const logContent = document.getElementById('logContent');
177-
201+
178202
// Add new logs to buffer, skipping duplicates
179203
data.logs.forEach(line => {
180204
if (line !== lastLine) {
181205
logBuffer.push(line);
182206
lastLine = line;
183207
}
184208
});
185-
209+
186210
// Trim buffer if it exceeds size
187211
if (logBuffer.length > BUFFER_SIZE) {
188212
logBuffer = logBuffer.slice(-BUFFER_SIZE);
189213
}
190-
214+
191215
// Update display
192216
updateLogDisplay();
193217
})
@@ -239,8 +263,10 @@ function restartFromEnd() {
239263
currentPosition = 0;
240264
logBuffer = [];
241265
isPaused = false;
266+
currentPollInterval = MIN_POLL_INTERVAL;
242267
document.getElementById('pauseButton').textContent = 'Pause';
243268
fetchLogs();
269+
scheduleFetch();
244270
}
245271
246272
// Start fetching logs when page loads
@@ -251,7 +277,7 @@ document.addEventListener('DOMContentLoaded', () => {
251277
252278
// Start log fetching
253279
fetchLogs();
254-
updateInterval = setInterval(fetchLogs, 1000);
280+
scheduleFetch();
255281
256282
// Hide loading message after first logs appear
257283
const observer = new MutationObserver((mutations) => {

0 commit comments

Comments
 (0)