Repository navigation
Expand file tree
/
Copy pathopenflow_logger.py
More file actions
124 lines (95 loc) · 4.03 KB
/
Copy pathopenflow_logger.py
File metadata and controls
124 lines (95 loc) · 4.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Structured logging for OpenFlow.
Two rotating files under ~/.openflow/:
- openflow.log (DEBUG+, every message, 5 MB × 3 rotation)
- errors.log (ERROR+ only, 1 MB × 5 rotation, easier to scan)
Use `get_logger(__name__)` anywhere; messages also mirror to stdout/stderr
which the LaunchAgent captures in launchd.{out,err}.log.
The existing `daemon._install_file_logger` monkey-patches builtins.print
to write to openflow.log. This module adds a real logging hierarchy on
top — preserving every existing print() while giving us structured
exception capture and ERROR-only triage.
"""
from __future__ import annotations
import logging
import os
import sys
import traceback
from logging.handlers import RotatingFileHandler
from pathlib import Path
_LOG_DIR = Path(os.path.expanduser("~/.openflow"))
_MAIN_LOG = _LOG_DIR / "openflow.log"
_ERROR_LOG = _LOG_DIR / "errors.log"
_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
_DATEFMT = "%Y-%m-%d %H:%M:%S"
_configured = False
class _LazyRotatingFileHandler(RotatingFileHandler):
"""Opens (and creates the log dir) on the first record, not at set-up:
importing a module that calls get_logger() must not touch ~/.openflow."""
def __init__(self, filename, **kwargs) -> None:
super().__init__(filename, delay=True, **kwargs)
def _open(self):
Path(self.baseFilename).parent.mkdir(parents=True, exist_ok=True)
return super()._open()
def _ensure_configured() -> None:
global _configured
if _configured:
return
root = logging.getLogger("openflow")
root.setLevel(logging.DEBUG)
root.propagate = False
# Clear stale handlers in case of double-init (subprocess + parent)
for h in list(root.handlers):
root.removeHandler(h)
fmt = logging.Formatter(_FORMAT, datefmt=_DATEFMT)
# Main rotating handler — all levels
main_h = _LazyRotatingFileHandler(_MAIN_LOG, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8")
main_h.setLevel(logging.DEBUG)
main_h.setFormatter(fmt)
root.addHandler(main_h)
# Error-only handler — easy triage
err_h = _LazyRotatingFileHandler(_ERROR_LOG, maxBytes=1 * 1024 * 1024, backupCount=5, encoding="utf-8")
err_h.setLevel(logging.ERROR)
err_h.setFormatter(fmt)
root.addHandler(err_h)
# Stream handler so launchd captures critical lines
stream_h = logging.StreamHandler(sys.stderr)
stream_h.setLevel(logging.WARNING)
stream_h.setFormatter(fmt)
root.addHandler(stream_h)
# Install excepthook so uncaught exceptions land in errors.log
sys.excepthook = _excepthook
_configured = True
def _excepthook(exc_type, exc, tb) -> None:
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc, tb)
return
root = logging.getLogger("openflow")
root.error("uncaught exception", exc_info=(exc_type, exc, tb))
def get_logger(name: str = "openflow") -> logging.Logger:
_ensure_configured()
if not name.startswith("openflow"):
name = f"openflow.{name}"
return logging.getLogger(name)
def log_exception(component: str, msg: str = "exception", exc: BaseException | None = None) -> None:
"""Convenience: log an ERROR with traceback. Pass `exc` from an except block."""
logger = get_logger(component)
if exc is not None:
logger.error(msg, exc_info=(type(exc), exc, exc.__traceback__))
else:
logger.error(msg, exc_info=True)
def tail_log(level: str = "all", n: int = 50) -> str:
"""Return the last N lines of openflow.log (or errors.log if level='errors')."""
path = _ERROR_LOG if level == "errors" else _MAIN_LOG
if not path.exists():
return f"(no log at {path})\n"
try:
with path.open("rb") as f:
f.seek(0, 2)
size = f.tell()
chunk = min(size, 64 * 1024)
f.seek(size - chunk)
data = f.read().decode("utf-8", errors="replace")
lines = data.splitlines()
return "\n".join(lines[-n:]) + "\n"
except Exception as e:
return f"(failed to read {path}: {e})\n"