Skip to content

Commit cfce28d

Browse files
committed
logging functions in separate module
1 parent c213d70 commit cfce28d

File tree

1 file changed

+246
-0
lines changed

1 file changed

+246
-0
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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

Comments
 (0)