Files
MeshTerm/meshtools/persistence/logging.py
T
Jean-Pierre MartineauandClaude Opus 4.8 43353f83a9 Initial commit: MeshTools with device discovery & selection
MeshTools is a Rich+Questionary interactive / Typer-scriptable toolkit for
tuning a MeshCore mesh over a serial companion device, with SQLite-logged runs
and Plotly visualizations. Includes the skeleton (trace, info, history),
TX-power optimization, and a MockDevice simulator (--mock).

Device discovery & selection:
- core/discovery.py: enumerate serial ports (pyserial), flag/sort likely LoRa
  hardware by USB vendor id, stable_id for replug-safe identity.
- core/device_store.py: remember the last successfully connected device in
  <config_dir>/devices.json (independent of --db).
- core/selection.py: non-interactive resolver (--port > profile > remembered >
  sole device > guidance error) for the CLI.
- ui/device_picker.py + menu startup: interactive picker preselecting the
  remembered default; banner reflects the choice.
- tools/devices.py: `meshtools devices` lists/marks attached devices.
- AppContext resolves the port and records last-known-good on connect success.

Fixes:
- theme.py: force UTF-8 on stdout/stderr so glyphs don't crash legacy consoles.
- connection.py: friendly message when meshcore is missing; pin meshcore>=2.3.
- trace/tx-optimize: fall back to free-text target entry when the device
  reports no contacts (autocomplete requires non-empty choices).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 17:02:34 -04:00

94 lines
2.6 KiB
Python

"""Structured logging setup.
Two sinks are configured: a Rich-formatted console handler for human-readable output,
and a JSON-lines file handler that captures every record for later replay/analysis.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Optional
from rich.console import Console
from rich.logging import RichHandler
_LOGGER_NAME = "meshtools"
class JsonlFormatter(logging.Formatter):
"""Format log records as one JSON object per line."""
def format(self, record: logging.LogRecord) -> str:
"""Serialize a log record to a compact JSON string.
Args:
record: The record to format.
Returns:
A single-line JSON document.
"""
payload = {
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"level": record.levelname,
"logger": record.name,
"msg": record.getMessage(),
}
if record.exc_info:
payload["exc"] = self.formatException(record.exc_info)
return json.dumps(payload)
def configure_logging(
console: Console,
log_dir: Path,
*,
level: int = logging.INFO,
quiet: bool = False,
) -> logging.Logger:
"""Configure and return the application logger.
Args:
console: The Rich console the handler should render to.
log_dir: Directory for the ``meshtools.log.jsonl`` event file.
level: Console log level.
quiet: When ``True``, suppress console output (file logging continues).
Returns:
The configured ``meshtools`` logger.
"""
logger = logging.getLogger(_LOGGER_NAME)
logger.setLevel(logging.DEBUG)
logger.handlers.clear()
logger.propagate = False
if not quiet:
rich_handler = RichHandler(
console=console, rich_tracebacks=True, show_path=False, markup=True
)
rich_handler.setLevel(level)
logger.addHandler(rich_handler)
log_dir.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_dir / "meshtools.log.jsonl", encoding="utf-8")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(JsonlFormatter())
logger.addHandler(file_handler)
return logger
def get_logger(name: Optional[str] = None) -> logging.Logger:
"""Return a child of the application logger.
Args:
name: Optional child name; when ``None`` the root app logger is returned.
Returns:
The requested logger.
"""
if name is None:
return logging.getLogger(_LOGGER_NAME)
return logging.getLogger(f"{_LOGGER_NAME}.{name}")