|
| 1 | +import functools |
| 2 | +import inspect |
| 3 | +import logging |
| 4 | +import os |
| 5 | +import sys |
| 6 | +import time |
| 7 | + |
| 8 | +from enum import IntFlag, auto |
| 9 | +from typing import Callable, Union |
| 10 | + |
| 11 | + |
| 12 | +class LoggingScope(IntFlag): |
| 13 | + """Enumeration of different logging scopes.""" |
| 14 | + NONE = 0 |
| 15 | + FUNC_ENTRY_EXIT = auto() # Function entry/exit logging |
| 16 | + DOWNLOAD = auto() # Logging related to file downloads |
| 17 | + VERIFICATION = auto() # Logging related to signature and checksum verification |
| 18 | + STATE_OPS = auto() # Logging related to tarball state operations |
| 19 | + GITHUB_OPS = auto() # Logging related to GitHub operations (PRs, issues, etc.) |
| 20 | + GROUP_OPS = auto() # Logging related to tarball group operations |
| 21 | + TASK_OPS = auto() # Logging related to task operations |
| 22 | + ERROR = auto() # Error logging (separate from other scopes for easier filtering) |
| 23 | + DEBUG = auto() # Debug-level logging (separate from other scopes for easier filtering) |
| 24 | + ALL = (FUNC_ENTRY_EXIT | DOWNLOAD | VERIFICATION | STATE_OPS | |
| 25 | + GITHUB_OPS | GROUP_OPS | TASK_OPS | ERROR | DEBUG) |
| 26 | + |
| 27 | + |
| 28 | +# Global setting for logging scopes |
| 29 | +ENABLED_LOGGING_SCOPES = LoggingScope.NONE |
| 30 | + |
| 31 | + |
| 32 | +# Global variable to track call stack depth |
| 33 | +_call_stack_depth = 0 |
| 34 | + |
| 35 | + |
| 36 | +def is_logging_scope_enabled(scope: LoggingScope) -> bool: |
| 37 | + """Check if a specific logging scope is enabled.""" |
| 38 | + return bool(ENABLED_LOGGING_SCOPES & scope) |
| 39 | + |
| 40 | + |
| 41 | +def log_function_entry_exit(logger: logging.Logger = None) -> Callable: |
| 42 | + """ |
| 43 | + Decorator that logs function entry and exit with timing information. |
| 44 | + Only logs if the FUNC_ENTRY_EXIT scope is enabled. |
| 45 | +
|
| 46 | + Args: |
| 47 | + logger: Optional logger instance. If not provided, uses the module's logger. |
| 48 | + """ |
| 49 | + def decorator(func): |
| 50 | + @functools.wraps(func) |
| 51 | + def wrapper(*args, **kwargs): |
| 52 | + global _call_stack_depth |
| 53 | + |
| 54 | + if not is_logging_scope_enabled(LoggingScope.FUNC_ENTRY_EXIT): |
| 55 | + return func(*args, **kwargs) |
| 56 | + |
| 57 | + if logger is None: |
| 58 | + log = logging.getLogger(func.__module__) |
| 59 | + else: |
| 60 | + log = logger |
| 61 | + |
| 62 | + # Get context information if available |
| 63 | + context = "" |
| 64 | + if len(args) > 0 and hasattr(args[0], 'object'): |
| 65 | + # For EessiTarball methods, show the tarball name and state |
| 66 | + tarball = args[0] |
| 67 | + filename = os.path.basename(tarball.object) |
| 68 | + |
| 69 | + # Format filename to show important parts |
| 70 | + if len(filename) > 30: |
| 71 | + parts = filename.split('-') |
| 72 | + if len(parts) >= 6: # Ensure we have all required parts |
| 73 | + # Get version, component, last part of architecture, and epoch |
| 74 | + version = parts[1] |
| 75 | + component = parts[2] |
| 76 | + arch_last = parts[-2].split('-')[-1] # Last part of architecture |
| 77 | + epoch = parts[-1] # includes file extension |
| 78 | + filename = f"{version}-{component}-{arch_last}-{epoch}" |
| 79 | + else: |
| 80 | + # Fallback to simple truncation if format doesn't match |
| 81 | + filename = f"{filename[:15]}...{filename[-12:]}" |
| 82 | + |
| 83 | + context = f" [{filename}" |
| 84 | + if hasattr(tarball, 'state'): |
| 85 | + context += f" in {tarball.state}" |
| 86 | + context += "]" |
| 87 | + |
| 88 | + # Create indentation based on call stack depth |
| 89 | + indent = " " * _call_stack_depth |
| 90 | + |
| 91 | + # Get file name and line number where the function is defined |
| 92 | + file_name = os.path.basename(inspect.getsourcefile(func)) |
| 93 | + source_lines, start_line = inspect.getsourcelines(func) |
| 94 | + # Find the line with the actual function definition |
| 95 | + def_line = next(i for i, line in enumerate(source_lines) if line.strip().startswith('def ')) |
| 96 | + def_line_no = start_line + def_line |
| 97 | + # Find the last non-empty line of the function |
| 98 | + last_line = next(i for i, line in enumerate(reversed(source_lines)) if line.strip()) |
| 99 | + last_line_no = start_line + len(source_lines) - 1 - last_line |
| 100 | + |
| 101 | + start_time = time.time() |
| 102 | + log.info(f"{indent}[FUNC_ENTRY_EXIT] Entering {func.__name__} at {file_name}:{def_line_no}{context}") |
| 103 | + _call_stack_depth += 1 |
| 104 | + try: |
| 105 | + result = func(*args, **kwargs) |
| 106 | + _call_stack_depth -= 1 |
| 107 | + end_time = time.time() |
| 108 | + # For normal returns, show the last line of the function |
| 109 | + log.info(f"{indent}[FUNC_ENTRY_EXIT] Leaving {func.__name__} at {file_name}:{last_line_no}" |
| 110 | + f"{context} (took {end_time - start_time:.2f}s)") |
| 111 | + return result |
| 112 | + except Exception as err: |
| 113 | + _call_stack_depth -= 1 |
| 114 | + end_time = time.time() |
| 115 | + # For exceptions, try to get the line number from the exception |
| 116 | + try: |
| 117 | + exc_line_no = err.__traceback__.tb_lineno |
| 118 | + except AttributeError: |
| 119 | + exc_line_no = last_line_no |
| 120 | + log.info(f"{indent}[FUNC_ENTRY_EXIT] Leaving {func.__name__} at {file_name}:{exc_line_no}" |
| 121 | + f"{context} with exception (took {end_time - start_time:.2f}s)") |
| 122 | + raise err |
| 123 | + return wrapper |
| 124 | + return decorator |
| 125 | + |
| 126 | + |
| 127 | +def log_message(scope, level, msg, *args, logger=None, **kwargs): |
| 128 | + """ |
| 129 | + Log a message if either: |
| 130 | + 1. The specified scope is enabled, OR |
| 131 | + 2. The current log level is equal to or higher than the specified level |
| 132 | +
|
| 133 | + Args: |
| 134 | + scope: LoggingScope value indicating which scope this logging belongs to |
| 135 | + level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
| 136 | + msg: Message to log |
| 137 | + logger: Optional logger instance. If not provided, uses the root logger. |
| 138 | + *args, **kwargs: Additional arguments to pass to the logging function |
| 139 | + """ |
| 140 | + log = logger or logging.getLogger() |
| 141 | + log_level = getattr(logging, level.upper()) |
| 142 | + |
| 143 | + # Check if either condition is met |
| 144 | + if not (is_logging_scope_enabled(scope) or log_level >= log.getEffectiveLevel()): |
| 145 | + return |
| 146 | + |
| 147 | + # Create indentation based on call stack depth |
| 148 | + indent = " " * _call_stack_depth |
| 149 | + # Add scope to the message |
| 150 | + scoped_msg = f"[{scope.name}] {msg}" |
| 151 | + indented_msg = f"{indent}{scoped_msg}" |
| 152 | + |
| 153 | + # If scope is enabled, use the temporary handler |
| 154 | + if is_logging_scope_enabled(scope): |
| 155 | + # Save original handlers |
| 156 | + original_handlers = list(log.handlers) |
| 157 | + |
| 158 | + # Create a temporary handler that accepts all levels |
| 159 | + temp_handler = logging.StreamHandler(sys.stdout) |
| 160 | + temp_handler.setLevel(logging.DEBUG) |
| 161 | + temp_handler.setFormatter(logging.Formatter('%(levelname)-8s: %(message)s')) |
| 162 | + |
| 163 | + try: |
| 164 | + # Remove existing handlers temporarily |
| 165 | + for handler in original_handlers: |
| 166 | + log.removeHandler(handler) |
| 167 | + |
| 168 | + # Add temporary handler |
| 169 | + log.addHandler(temp_handler) |
| 170 | + |
| 171 | + # Log the message |
| 172 | + log_func = getattr(log, level.lower()) |
| 173 | + log_func(indented_msg, *args, **kwargs) |
| 174 | + finally: |
| 175 | + log.removeHandler(temp_handler) |
| 176 | + # Restore original handlers |
| 177 | + for handler in original_handlers: |
| 178 | + if handler not in log.handlers: |
| 179 | + log.addHandler(handler) |
| 180 | + # Only use normal logging if scope is not enabled AND level is high enough |
| 181 | + elif not is_logging_scope_enabled(scope) and log_level >= log.getEffectiveLevel(): |
| 182 | + # Use normal logging with level check |
| 183 | + log_func = getattr(log, level.lower()) |
| 184 | + log_func(indented_msg, *args, **kwargs) |
| 185 | + |
| 186 | + |
| 187 | +def set_logging_scopes(scopes: Union[LoggingScope, str, list[str]]) -> None: |
| 188 | + """ |
| 189 | + Set the enabled logging scopes. |
| 190 | +
|
| 191 | + Args: |
| 192 | + scopes: Can be |
| 193 | + - A LoggingScope value |
| 194 | + - A string with comma-separated values using +/- syntax: |
| 195 | + - "+SCOPE" to enable a scope |
| 196 | + - "-SCOPE" to disable a scope |
| 197 | + - "ALL" or "+ALL" to enable all scopes |
| 198 | + - "-ALL" to disable all scopes |
| 199 | + Examples: |
| 200 | + "+FUNC_ENTRY_EXIT" # Enable only function entry/exit |
| 201 | + "+FUNC_ENTRY_EXIT,-EXAMPLE_SCOPE" # Enable function entry/exit but disable example |
| 202 | + "+ALL,-FUNC_ENTRY_EXIT" # Enable all scopes except function entry/exit |
| 203 | + """ |
| 204 | + global ENABLED_LOGGING_SCOPES |
| 205 | + |
| 206 | + if isinstance(scopes, LoggingScope): |
| 207 | + ENABLED_LOGGING_SCOPES = scopes |
| 208 | + return |
| 209 | + |
| 210 | + if isinstance(scopes, str): |
| 211 | + # Start with no scopes enabled |
| 212 | + ENABLED_LOGGING_SCOPES = LoggingScope.NONE |
| 213 | + |
| 214 | + # Split into individual scope specifications |
| 215 | + scope_specs = [s.strip() for s in scopes.split(",")] |
| 216 | + |
| 217 | + for spec in scope_specs: |
| 218 | + if not spec: |
| 219 | + continue |
| 220 | + |
| 221 | + # Check for ALL special case |
| 222 | + if spec.upper() in ["ALL", "+ALL"]: |
| 223 | + ENABLED_LOGGING_SCOPES = LoggingScope.ALL |
| 224 | + continue |
| 225 | + elif spec.upper() == "-ALL": |
| 226 | + ENABLED_LOGGING_SCOPES = LoggingScope.NONE |
| 227 | + continue |
| 228 | + |
| 229 | + # Parse scope name and operation |
| 230 | + operation = spec[0] |
| 231 | + scope_name = spec[1:].strip().upper() |
| 232 | + |
| 233 | + try: |
| 234 | + scope_enum = LoggingScope[scope_name] |
| 235 | + if operation == '+': |
| 236 | + ENABLED_LOGGING_SCOPES |= scope_enum |
| 237 | + elif operation == '-': |
| 238 | + ENABLED_LOGGING_SCOPES &= ~scope_enum |
| 239 | + else: |
| 240 | + logging.warning(f"Invalid operation '{operation}' in scope specification: {spec}") |
| 241 | + except KeyError: |
| 242 | + logging.warning(f"Unknown logging scope: {scope_name}") |
| 243 | + |
| 244 | + elif isinstance(scopes, list): |
| 245 | + # Convert list to comma-separated string and process |
| 246 | + set_logging_scopes(",".join(scopes)) |
0 commit comments