Coverage for daklib/daklog.py: 93%
72 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-05-10 21:38 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-05-10 21:38 +0000
1"""
2Logging functions
4@contact: Debian FTP Master <ftpmaster@debian.org>
5@copyright: 2001, 2002, 2006 James Troup <james@nocrew.org>
6@license: GNU General Public License version 2 or later
7"""
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23################################################################################
25import fcntl
26import logging
27import os
28import sys
29import time
30import traceback
31from typing import Any, override
33from . import utils
35################################################################################
38class Logger:
39 "Logger object"
41 __shared_state: dict[str, Any] = {}
43 def __init__(
44 self, program="unknown", debug=False, print_starting=True, include_pid=False
45 ):
46 self.__dict__ = self.__shared_state
48 self.program = program
49 self.debug = debug
50 self.include_pid = include_pid
52 if not getattr(self, "logfile", None):
53 self._open_log(debug)
55 if not getattr(self, "_handler", None):
56 self._handler = DakLogHandler(self)
57 logging.getLogger().addHandler(self._handler)
58 logging.getLogger().setLevel(logging.INFO)
60 if print_starting:
61 self.log(["program start"])
63 def _open_log(self, debug: bool) -> None:
64 # Create the log directory if it doesn't exist
65 from daklib.config import Config
67 logdir = Config()["Dir::Log"]
68 if not os.path.exists(logdir):
69 umask = os.umask(00000)
70 os.makedirs(logdir, 0o2775)
71 os.umask(umask)
73 # Open the logfile
74 logfilename = "%s/%s" % (logdir, time.strftime("%Y-%m"))
75 logfile = None
77 if debug: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true
78 logfile = sys.stderr
79 else:
80 umask = os.umask(0o0002)
81 logfile = open(logfilename, "a")
82 os.umask(umask)
84 self.logfile = logfile
86 def log(self, details: list[object]) -> None:
87 "Log an event"
88 # Prepend timestamp, program name, and user name
89 details_str = [time.strftime("%Y%m%d%H%M%S"), self.program, utils.getusername()]
90 # Force the contents of the list to be string.join-able
91 details_str.extend(str(i) for i in details)
92 fcntl.lockf(self.logfile, fcntl.LOCK_EX)
93 # Write out the log in TSV
94 self.logfile.write("|".join(details_str) + "\n")
95 # Flush the output to enable tail-ing
96 self.logfile.flush()
97 fcntl.lockf(self.logfile, fcntl.LOCK_UN)
99 def log_traceback(self, info, ex) -> None:
100 "Log an exception with a traceback"
101 self.log([info, repr(ex)])
102 for line in traceback.format_exc().split("\n")[:-1]:
103 self.log(["traceback", line])
105 def close(self) -> None:
106 "Close a Logger object"
107 self.log(["program end"])
108 self.logfile.close()
111class DakLogHandler(logging.Handler):
112 """logging.Handler that writes log records through a daklog Logger."""
114 def __init__(self, logger: Logger, level: int = logging.NOTSET) -> None:
115 super().__init__(level)
116 self._daklogger = logger
118 @override
119 def emit(self, record: logging.LogRecord) -> None:
120 try:
121 # Format the message without exception info so tracebacks stay on
122 # their own "traceback|..." lines in the daklog TSV below.
123 saved_exc_info, saved_exc_text = record.exc_info, record.exc_text
124 record.exc_info = None
125 record.exc_text = None
126 try:
127 msg = self.format(record)
128 finally:
129 record.exc_info, record.exc_text = saved_exc_info, saved_exc_text
130 self._daklogger.log([msg])
131 if isinstance(record.exc_info, tuple) and record.exc_info[1] is not None:
132 for line in traceback.format_exception(*record.exc_info):
133 for subline in line.rstrip("\n").split("\n"):
134 self._daklogger.log(["traceback", subline])
135 except Exception:
136 self.handleError(record)