Skip to content

Commit 87b74f3

Browse files
Merge pull request #7607 from ThomasWaldmann/new-remote-logging
new remote and progress logging, cleanups
2 parents ca68dd2 + e2ea5cf commit 87b74f3

File tree

9 files changed

+244
-405
lines changed

9 files changed

+244
-405
lines changed

src/borg/archiver/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,9 @@ def run(self, args):
482482
func = get_func(args)
483483
# do not use loggers before this!
484484
is_serve = func == self.do_serve
485-
self.log_json = args.log_json or is_serve
486-
setup_logging(level=args.log_level, json=self.log_json)
485+
self.log_json = args.log_json and not is_serve
486+
func_name = getattr(func, "__name__", "none")
487+
setup_logging(level=args.log_level, is_serve=is_serve, log_json=self.log_json, func=func_name)
487488
args.progress |= is_serve
488489
self._setup_implied_logging(vars(args))
489490
self._setup_topic_debugging(args)

src/borg/helpers/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .process import daemonize, daemonizing
3434
from .process import signal_handler, raising_signal_handler, sig_int, ignore_sigint, SigHup, SigTerm
3535
from .process import popen_with_error_handling, is_terminal, prepare_subprocess_env, create_filter_process
36-
from .progress import ProgressIndicatorPercent, ProgressIndicatorEndless, ProgressIndicatorMessage
36+
from .progress import ProgressIndicatorPercent, ProgressIndicatorMessage
3737
from .time import parse_timestamp, timestamp, safe_timestamp, safe_s, safe_ns, MAX_S, SUPPORT_32BIT_PLATFORMS
3838
from .time import format_time, format_timedelta, OutputTimestamp, archive_ts_now
3939
from .yes_no import yes, TRUISH, FALSISH, DEFAULTISH

src/borg/helpers/progress.py

Lines changed: 12 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
import logging
22
import json
3-
import sys
43
import time
5-
from shutil import get_terminal_size
64

75
from ..logger import create_logger
86

97
logger = create_logger()
108

11-
from .parseformat import ellipsis_truncate
12-
13-
14-
def justify_to_terminal_size(message):
15-
terminal_space = get_terminal_size(fallback=(-1, -1))[0]
16-
# justify only if we are outputting to a terminal
17-
if terminal_space != -1:
18-
return message.ljust(terminal_space)
19-
return message
20-
219

2210
class ProgressIndicatorBase:
2311
LOGGER = "borg.output.progress"
2412
JSON_TYPE: str = None
25-
json = False
2613

2714
operation_id_counter = 0
2815

@@ -33,73 +20,27 @@ def operation_id(cls):
3320
return cls.operation_id_counter
3421

3522
def __init__(self, msgid=None):
36-
self.handler = None
3723
self.logger = logging.getLogger(self.LOGGER)
3824
self.id = self.operation_id()
3925
self.msgid = msgid
4026

41-
# If there are no handlers, set one up explicitly because the
42-
# terminator and propagation needs to be set. If there are,
43-
# they must have been set up by BORG_LOGGING_CONF: skip setup.
44-
if not self.logger.handlers:
45-
self.handler = logging.StreamHandler(stream=sys.stderr)
46-
self.handler.setLevel(logging.INFO)
47-
logger = logging.getLogger("borg")
48-
# Some special attributes on the borg logger, created by setup_logging
49-
# But also be able to work without that
50-
try:
51-
formatter = logger.formatter
52-
terminator = "\n" if logger.json else "\r"
53-
self.json = logger.json
54-
except AttributeError:
55-
terminator = "\r"
56-
else:
57-
self.handler.setFormatter(formatter)
58-
self.handler.terminator = terminator
59-
60-
self.logger.addHandler(self.handler)
61-
if self.logger.level == logging.NOTSET:
62-
self.logger.setLevel(logging.WARN)
63-
self.logger.propagate = False
64-
65-
# If --progress is not set then the progress logger level will be WARN
66-
# due to setup_implied_logging (it may be NOTSET with a logging config file,
67-
# but the interactions there are generally unclear), so self.emit becomes
68-
# False, which is correct.
69-
# If --progress is set then the level will be INFO as per setup_implied_logging;
70-
# note that this is always the case for serve processes due to a "args.progress |= is_serve".
71-
# In this case self.emit is True.
72-
self.emit = self.logger.getEffectiveLevel() == logging.INFO
73-
74-
def __del__(self):
75-
if self.handler is not None:
76-
self.logger.removeHandler(self.handler)
77-
self.handler.close()
78-
79-
def output_json(self, *, finished=False, **kwargs):
80-
assert self.json
81-
if not self.emit:
82-
return
27+
def make_json(self, *, finished=False, **kwargs):
8328
kwargs.update(
8429
dict(operation=self.id, msgid=self.msgid, type=self.JSON_TYPE, finished=finished, time=time.time())
8530
)
86-
print(json.dumps(kwargs), file=sys.stderr, flush=True)
31+
return json.dumps(kwargs)
8732

8833
def finish(self):
89-
if self.json:
90-
self.output_json(finished=True)
91-
else:
92-
self.output("")
34+
j = self.make_json(message="", finished=True)
35+
self.logger.info(j)
9336

9437

9538
class ProgressIndicatorMessage(ProgressIndicatorBase):
9639
JSON_TYPE = "progress_message"
9740

9841
def output(self, msg):
99-
if self.json:
100-
self.output_json(message=msg)
101-
else:
102-
self.logger.info(justify_to_terminal_size(msg))
42+
j = self.make_json(message=msg)
43+
self.logger.info(j)
10344

10445

10546
class ProgressIndicatorPercent(ProgressIndicatorBase):
@@ -141,58 +82,11 @@ def show(self, current=None, increase=1, info=None):
14182
"""
14283
pct = self.progress(current, increase)
14384
if pct is not None:
144-
# truncate the last argument, if no space is available
14585
if info is not None:
146-
if not self.json:
147-
from ..platform import swidth # avoid circular import
148-
149-
# no need to truncate if we're not outputting to a terminal
150-
terminal_space = get_terminal_size(fallback=(-1, -1))[0]
151-
if terminal_space != -1:
152-
space = terminal_space - swidth(self.msg % tuple([pct] + info[:-1] + [""]))
153-
info[-1] = ellipsis_truncate(info[-1], space)
154-
return self.output(self.msg % tuple([pct] + info), justify=False, info=info)
155-
156-
return self.output(self.msg % pct)
157-
158-
def output(self, message, justify=True, info=None):
159-
if self.json:
160-
self.output_json(message=message, current=self.counter, total=self.total, info=info)
161-
else:
162-
if justify:
163-
message = justify_to_terminal_size(message)
164-
self.logger.info(message)
165-
166-
167-
class ProgressIndicatorEndless:
168-
def __init__(self, step=10, file=None):
169-
"""
170-
Progress indicator (long row of dots)
171-
172-
:param step: every Nth call, call the func
173-
:param file: output file, default: sys.stderr
174-
"""
175-
self.counter = 0 # call counter
176-
self.triggered = 0 # increases 1 per trigger event
177-
self.step = step # trigger every <step> calls
178-
if file is None:
179-
file = sys.stderr
180-
self.file = file
181-
182-
def progress(self):
183-
self.counter += 1
184-
trigger = self.counter % self.step == 0
185-
if trigger:
186-
self.triggered += 1
187-
return trigger
188-
189-
def show(self):
190-
trigger = self.progress()
191-
if trigger:
192-
return self.output(self.triggered)
193-
194-
def output(self, triggered):
195-
print(".", end="", file=self.file, flush=True)
86+
return self.output(self.msg % tuple([pct] + info), info=info)
87+
else:
88+
return self.output(self.msg % pct)
19689

197-
def finish(self):
198-
print(file=self.file)
90+
def output(self, message, info=None):
91+
j = self.make_json(message=message, current=self.counter, total=self.total, info=info)
92+
self.logger.info(j)

src/borg/logger.py

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,64 @@
3636
import logging.config
3737
import logging.handlers # needed for handlers defined there being configurable in logging.conf file
3838
import os
39+
import queue
40+
import sys
41+
import time
42+
from typing import Optional
3943
import warnings
4044

45+
logging_debugging_path: Optional[str] = None # if set, write borg.logger debugging log to path/borg-*.log
46+
4147
configured = False
48+
borg_serve_log_queue: queue.SimpleQueue = queue.SimpleQueue()
49+
50+
51+
class BorgQueueHandler(logging.handlers.QueueHandler):
52+
"""borg serve writes log record dicts to a borg_serve_log_queue"""
53+
54+
def prepare(self, record: logging.LogRecord) -> dict:
55+
return dict(
56+
# kwargs needed for LogRecord constructor:
57+
name=record.name,
58+
level=record.levelno,
59+
pathname=record.pathname,
60+
lineno=record.lineno,
61+
msg=record.msg,
62+
args=record.args,
63+
exc_info=record.exc_info,
64+
func=record.funcName,
65+
sinfo=record.stack_info,
66+
)
67+
68+
69+
class StderrHandler(logging.StreamHandler):
70+
"""
71+
This class is like a StreamHandler using sys.stderr, but always uses
72+
whatever sys.stderr is currently set to rather than the value of
73+
sys.stderr at handler construction time.
74+
"""
75+
76+
def __init__(self, stream=None):
77+
logging.Handler.__init__(self)
78+
79+
@property
80+
def stream(self):
81+
return sys.stderr
82+
83+
84+
class TextProgressFormatter(logging.Formatter):
85+
def format(self, record: logging.LogRecord) -> str:
86+
# record.msg contains json (because we always do json for progress log)
87+
j = json.loads(record.msg)
88+
# inside the json, the text log line can be found under "message"
89+
return f"{j['message']}"
90+
91+
92+
class JSONProgressFormatter(logging.Formatter):
93+
def format(self, record: logging.LogRecord) -> str:
94+
# record.msg contains json (because we always do json for progress log)
95+
return f"{record.msg}"
96+
4297

4398
# use something like this to ignore warnings:
4499
# warnings.filterwarnings('ignore', r'... regex for warning message to ignore ...')
@@ -53,14 +108,31 @@ def _log_warning(message, category, filename, lineno, file=None, line=None):
53108
logger.warning(msg)
54109

55110

56-
def setup_logging(stream=None, conf_fname=None, env_var="BORG_LOGGING_CONF", level="info", json=False):
111+
def remove_handlers(logger):
112+
for handler in logger.handlers[:]:
113+
handler.flush()
114+
handler.close()
115+
logger.removeHandler(handler)
116+
117+
118+
def teardown_logging():
119+
global configured
120+
logging.shutdown()
121+
configured = False
122+
123+
124+
def setup_logging(
125+
stream=None, conf_fname=None, env_var="BORG_LOGGING_CONF", level="info", is_serve=False, log_json=False, func=None
126+
):
57127
"""setup logging module according to the arguments provided
58128
59129
if conf_fname is given (or the config file name can be determined via
60130
the env_var, if given): load this logging configuration.
61131
62132
otherwise, set up a stream handler logger on stderr (by default, if no
63133
stream is provided).
134+
135+
is_serve: are we setting up the logging for "borg serve"?
64136
"""
65137
global configured
66138
err_msg = None
@@ -77,25 +149,56 @@ def setup_logging(stream=None, conf_fname=None, env_var="BORG_LOGGING_CONF", lev
77149
logging.config.fileConfig(f)
78150
configured = True
79151
logger = logging.getLogger(__name__)
80-
borg_logger = logging.getLogger("borg")
81-
borg_logger.json = json
82152
logger.debug(f'using logging configuration read from "{conf_fname}"')
83153
warnings.showwarning = _log_warning
84154
return None
85155
except Exception as err: # XXX be more precise
86156
err_msg = str(err)
157+
87158
# if we did not / not successfully load a logging configuration, fallback to this:
88-
logger = logging.getLogger("")
89-
handler = logging.StreamHandler(stream)
159+
level = level.upper()
90160
fmt = "%(message)s"
91-
formatter = JsonFormatter(fmt) if json else logging.Formatter(fmt)
161+
formatter = JsonFormatter(fmt) if log_json else logging.Formatter(fmt)
162+
SHandler = StderrHandler if stream is None else logging.StreamHandler
163+
handler = BorgQueueHandler(borg_serve_log_queue) if is_serve else SHandler(stream)
92164
handler.setFormatter(formatter)
93-
borg_logger = logging.getLogger("borg")
94-
borg_logger.formatter = formatter
95-
borg_logger.json = json
96-
logger.addHandler(handler)
97-
logger.setLevel(level.upper())
165+
logger = logging.getLogger()
166+
remove_handlers(logger)
167+
logger.setLevel(level)
168+
169+
if logging_debugging_path is not None:
170+
# add an addtl. root handler for debugging purposes
171+
log_fname = os.path.join(logging_debugging_path, f"borg-{'serve' if is_serve else 'client'}-root.log")
172+
handler2 = logging.StreamHandler(open(log_fname, "a"))
173+
handler2.setFormatter(formatter)
174+
logger.addHandler(handler2)
175+
logger.warning(f"--- {func} ---") # only handler2 shall get this
176+
177+
logger.addHandler(handler) # do this late, so handler is not added while debug handler is set up
178+
179+
bop_formatter = JSONProgressFormatter() if log_json else TextProgressFormatter()
180+
bop_handler = BorgQueueHandler(borg_serve_log_queue) if is_serve else SHandler(stream)
181+
bop_handler.setFormatter(bop_formatter)
182+
bop_logger = logging.getLogger("borg.output.progress")
183+
remove_handlers(bop_logger)
184+
bop_logger.setLevel("INFO")
185+
bop_logger.propagate = False
186+
187+
if logging_debugging_path is not None:
188+
# add an addtl. progress handler for debugging purposes
189+
log_fname = os.path.join(logging_debugging_path, f"borg-{'serve' if is_serve else 'client'}-progress.log")
190+
bop_handler2 = logging.StreamHandler(open(log_fname, "a"))
191+
bop_handler2.setFormatter(bop_formatter)
192+
bop_logger.addHandler(bop_handler2)
193+
json_dict = dict(
194+
message=f"--- {func} ---", operation=0, msgid="", type="progress_message", finished=False, time=time.time()
195+
)
196+
bop_logger.warning(json.dumps(json_dict)) # only bop_handler2 shall get this
197+
198+
bop_logger.addHandler(bop_handler) # do this late, so bop_handler is not added while debug handler is set up
199+
98200
configured = True
201+
99202
logger = logging.getLogger(__name__)
100203
if err_msg:
101204
logger.warning(f'setup_logging for "{conf_fname}" failed with "{err_msg}".')

0 commit comments

Comments
 (0)