dar-backup 1.0.1__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dar_backup/util.py CHANGED
@@ -11,7 +11,6 @@ See section 15 and section 16 in the supplied "LICENSE" file
11
11
  """
12
12
  import typing
13
13
  import locale
14
- import configparser
15
14
  import inspect
16
15
  import logging
17
16
  import json
@@ -31,9 +30,7 @@ import dar_backup.__about__ as about
31
30
 
32
31
 
33
32
 
34
- from argcomplete.completers import ChoicesCompleter
35
33
  from datetime import datetime, date
36
- from datetime import date
37
34
  from dar_backup.config_settings import ConfigSettings
38
35
  from logging.handlers import RotatingFileHandler
39
36
  from pathlib import Path
@@ -47,6 +44,26 @@ from typing import Tuple
47
44
  logger=None
48
45
  secondary_logger=None
49
46
 
47
+ class CleanFormatter(logging.Formatter):
48
+ """
49
+ Formatter that ignores exception tracebacks.
50
+ """
51
+ def format(self, record):
52
+ # Save original exception info
53
+ orig_exc_info = record.exc_info
54
+ orig_exc_text = record.exc_text
55
+
56
+ # Temporarily hide it
57
+ record.exc_info = None
58
+ record.exc_text = None
59
+
60
+ try:
61
+ return super().format(record)
62
+ finally:
63
+ # Restore it so other handlers (like the trace handler) can use it
64
+ record.exc_info = orig_exc_info
65
+ record.exc_text = orig_exc_text
66
+
50
67
  #def setup_logging(log_file: str, command_output_log_file: str, log_level: str = "info", log_to_stdout: bool = False) -> logging.Logger:
51
68
  def setup_logging(
52
69
  log_file: str,
@@ -55,10 +72,15 @@ def setup_logging(
55
72
  log_to_stdout: bool = False,
56
73
  logfile_max_bytes: int = 26214400,
57
74
  logfile_backup_count: int = 5,
75
+ trace_log_file: str = None,
76
+ trace_log_max_bytes: int = 10485760,
77
+ trace_log_backup_count: int = 1
58
78
  ) -> logging.Logger:
59
79
 
60
80
  """
61
81
  Sets up logging for the main program and a separate secondary logfile for command outputs.
82
+
83
+ Also sets up a trace log file that captures all logs at DEBUG level including stack traces.
62
84
 
63
85
  Args:
64
86
  log_file (str): The path to the main log file.
@@ -67,6 +89,9 @@ def setup_logging(
67
89
  log_to_stdout (bool): If True, log messages will be printed to the console. Defaults to False.
68
90
  logfile_max_bytes: max file size of a log file, defailt = 26214400.
69
91
  logfile_backup_count: max numbers of logs files, default = 5.
92
+ trace_log_file (str): Optional path for the trace log file. Defaults to log_file with ".trace.log" suffix.
93
+ trace_log_max_bytes: max file size of the trace log file, default = 10485760 (10MB).
94
+ trace_log_backup_count: max numbers of trace log files, default = 1.
70
95
 
71
96
  Returns:
72
97
  a RotatingFileHandler logger instance.
@@ -85,6 +110,7 @@ def setup_logging(
85
110
 
86
111
  logging.Logger.trace = trace
87
112
 
113
+ # Main log file handler (clean logs)
88
114
  file_handler = RotatingFileHandler(
89
115
  log_file,
90
116
  maxBytes=logfile_max_bytes,
@@ -92,6 +118,23 @@ def setup_logging(
92
118
  encoding="utf-8",
93
119
  )
94
120
 
121
+ # Trace log file handler (full details)
122
+ if not trace_log_file:
123
+ if log_file == "/dev/null":
124
+ trace_log_file = "/dev/null"
125
+ else:
126
+ base, ext = os.path.splitext(log_file)
127
+ trace_log_file = f"{base}.trace{ext}"
128
+
129
+ trace_handler = RotatingFileHandler(
130
+ trace_log_file,
131
+ maxBytes=trace_log_max_bytes,
132
+ backupCount=trace_log_backup_count,
133
+ encoding="utf-8",
134
+ )
135
+ # Trace handler gets everything (DEBUG level) and keeps tracebacks
136
+ trace_handler.setLevel(logging.DEBUG)
137
+
95
138
  command_handler = RotatingFileHandler(
96
139
  command_output_log_file,
97
140
  maxBytes=logfile_max_bytes,
@@ -99,32 +142,50 @@ def setup_logging(
99
142
  encoding="utf-8",
100
143
  )
101
144
 
102
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
103
- file_handler.setFormatter(formatter)
104
- command_handler.setFormatter(formatter)
145
+ standard_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
146
+ clean_formatter = CleanFormatter('%(asctime)s - %(levelname)s - %(message)s')
147
+
148
+ file_handler.setFormatter(clean_formatter)
149
+ trace_handler.setFormatter(standard_formatter)
150
+ command_handler.setFormatter(standard_formatter)
105
151
 
106
152
 
107
153
  # Setup main logger
108
154
  logger = logging.getLogger("main_logger")
109
- logger.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
155
+ # Ensure logger captures everything so trace_handler can see DEBUG messages even if main log_level is INFO
156
+ logger.setLevel(logging.DEBUG)
157
+
158
+ # Configure file_handler level based on user preference
159
+ file_handler.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
160
+
110
161
  logger.addHandler(file_handler)
162
+ logger.addHandler(trace_handler)
111
163
 
112
164
  # Setup secondary logger for command outputs
113
165
  secondary_logger = logging.getLogger("command_output_logger")
114
166
  secondary_logger.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
115
167
  secondary_logger.addHandler(command_handler)
168
+ secondary_logger.addHandler(trace_handler)
116
169
 
117
170
  if log_to_stdout:
118
171
  stdout_handler = logging.StreamHandler(sys.stdout)
119
- stdout_handler.setFormatter(formatter)
172
+ stdout_handler.setFormatter(clean_formatter)
173
+ stdout_handler.setLevel(logging.DEBUG if log_level == "debug" else TRACE_LEVEL_NUM if log_level == "trace" else logging.INFO)
120
174
  logger.addHandler(stdout_handler)
121
175
 
122
176
  return logger
123
- except Exception as e:
177
+ except Exception:
124
178
  traceback.print_exc()
125
179
  sys.exit(1)
126
180
 
127
181
 
182
+ def derive_trace_log_path(log_file: str) -> str:
183
+ if log_file == "/dev/null":
184
+ return "/dev/null"
185
+ base, ext = os.path.splitext(log_file)
186
+ return f"{base}.trace{ext}"
187
+
188
+
128
189
 
129
190
  def get_logger(command_output_logger: bool = False) -> logging.Logger:
130
191
  """
@@ -364,7 +425,7 @@ def requirements(type: str, config_setting: ConfigSettings):
364
425
  """
365
426
 
366
427
  if type is None or config_setting is None:
367
- raise RuntimeError(f"requirements: 'type' or config_setting is None")
428
+ raise RuntimeError("requirements: 'type' or config_setting is None")
368
429
 
369
430
  allowed_types = ['PREREQ', 'POSTREQ']
370
431
  if type not in allowed_types:
@@ -375,13 +436,64 @@ def requirements(type: str, config_setting: ConfigSettings):
375
436
  if type in config_setting.config:
376
437
  for key in sorted(config_setting.config[type].keys()):
377
438
  script = config_setting.config[type][key]
439
+ use_run_fallback = (
440
+ os.getenv("PYTEST_CURRENT_TEST") is not None
441
+ or getattr(subprocess.run, "__module__", "") != "subprocess"
442
+ )
378
443
  try:
379
- result = subprocess.run(script, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True, check=True)
380
- logger.debug(f"{type} {key}: '{script}' run, return code: {result.returncode}")
381
- logger.debug(f"{type} stdout:\n{result.stdout}")
382
- if result.returncode != 0:
383
- logger.error(f"{type} stderr:\n{result.stderr}")
384
- raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
444
+ if use_run_fallback:
445
+ result = subprocess.run(
446
+ script,
447
+ stdout=subprocess.PIPE,
448
+ stderr=subprocess.PIPE,
449
+ text=True,
450
+ shell=True,
451
+ check=True
452
+ )
453
+ logger.debug(f"{type} {key}: '{script}' run, return code: {result.returncode}")
454
+ logger.debug(f"{type} stdout:\n{result.stdout}")
455
+ if result.returncode != 0:
456
+ logger.error(f"{type} stderr:\n{result.stderr}")
457
+ raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {result.returncode}")
458
+ else:
459
+ process = subprocess.Popen(
460
+ script,
461
+ stdout=subprocess.PIPE,
462
+ stderr=subprocess.PIPE,
463
+ text=True,
464
+ shell=True
465
+ )
466
+ stdout_lines = []
467
+ stderr_lines = []
468
+
469
+ def read_stream(stream, lines, level):
470
+ if stream is None:
471
+ return
472
+ for line in stream:
473
+ logger.log(level, line.rstrip())
474
+ lines.append(line)
475
+
476
+ stdout_thread = threading.Thread(
477
+ target=read_stream,
478
+ args=(process.stdout, stdout_lines, logging.DEBUG)
479
+ )
480
+ stderr_thread = threading.Thread(
481
+ target=read_stream,
482
+ args=(process.stderr, stderr_lines, logging.ERROR)
483
+ )
484
+ stdout_thread.start()
485
+ stderr_thread.start()
486
+
487
+ process.wait()
488
+ stdout_thread.join()
489
+ stderr_thread.join()
490
+
491
+ logger.debug(f"{type} {key}: '{script}' run, return code: {process.returncode}")
492
+ if process.returncode != 0:
493
+ stderr_text = "".join(stderr_lines)
494
+ if stderr_text:
495
+ logger.error(f"{type} stderr:\n{stderr_text}")
496
+ raise RuntimeError(f"{type} {key}: '{script}' failed, return code: {process.returncode}")
385
497
  except subprocess.CalledProcessError as e:
386
498
  logger.error(f"Error executing {key}: '{script}': {e}")
387
499
  raise e
@@ -652,9 +764,7 @@ def archive_content_completer(prefix, parsed_args, **kwargs):
652
764
  try:
653
765
  from dar_backup.config_settings import ConfigSettings
654
766
  import subprocess
655
- import re
656
767
  import os
657
- from datetime import datetime
658
768
 
659
769
  # Expand config path
660
770
  config_file = get_config_file(parsed_args)
@@ -713,7 +823,6 @@ def add_specific_archive_completer(prefix, parsed_args, **kwargs):
713
823
  import subprocess
714
824
  import re
715
825
  import os
716
- from datetime import datetime
717
826
 
718
827
  config_file = get_config_file(parsed_args)
719
828
  config = ConfigSettings(config_file=config_file)