Skip to content

Logging

Custom logging configuration for pyfabricops.

T # Colored symbols for each log level SYMBOLS = { 'DEBUG': ESC + '36m○', # Cyan circle 'INFO': ESC + '34mi', # Blue "i" 'SUCCESS': ESC + '32m✓', # Green check 'WARNING': ESC + '33m△', # Yellow triangle 'ERROR': ESC + '31m✕', # Red X 'CRITICAL': ESC + '41;97m⊗', # Red background + bright white circled times }e provides a centralized logging system with customizable formatters, handlers, and configuration options for better debugging and monitoring.

PyFabricOpsFilter

Bases: Filter

Custom filter to control which log records are processed.

Source code in src/pyfabricops/utils/logging.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
class PyFabricOpsFilter(logging.Filter):
    """Custom filter to control which log records are processed."""

    def __init__(self, include_external: bool = False):
        """
        Initialize the filter.

        Args:
            include_external (bool): Whether to include logs from external libraries
        """
        super().__init__()
        self.include_external = include_external

    def filter(self, record: logging.LogRecord) -> bool:
        """Filter log records based on configuration."""
        # Always include pyfabricops logs
        if record.name.startswith('pyfabricops'):
            return True

        # Include external logs only if explicitly enabled
        return self.include_external

__init__(include_external=False)

Initialize the filter.

Parameters:

Name Type Description Default
include_external bool

Whether to include logs from external libraries

False
Source code in src/pyfabricops/utils/logging.py
187
188
189
190
191
192
193
194
195
def __init__(self, include_external: bool = False):
    """
    Initialize the filter.

    Args:
        include_external (bool): Whether to include logs from external libraries
    """
    super().__init__()
    self.include_external = include_external

filter(record)

Filter log records based on configuration.

Source code in src/pyfabricops/utils/logging.py
197
198
199
200
201
202
203
204
def filter(self, record: logging.LogRecord) -> bool:
    """Filter log records based on configuration."""
    # Always include pyfabricops logs
    if record.name.startswith('pyfabricops'):
        return True

    # Include external logs only if explicitly enabled
    return self.include_external

PyFabricOpsFormatter

Bases: Formatter

Custom formatter for pyfabricops with colored output and structured format.

Source code in src/pyfabricops/utils/logging.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class PyFabricOpsFormatter(logging.Formatter):
    """Custom formatter for pyfabricops with colored output and structured format."""

    ESC = '\x1b['

    COLORS = {
        'DEBUG': ESC + '36m',  # Cyan text
        'INFO': ESC + '34m',  # Blue text
        'SUCCESS': ESC + '32m',  # Green text
        'WARNING': ESC + '33m',  # Yellow text
        'ERROR': ESC + '31m',  # Red text
        'CRITICAL': ESC + '31m',  # Red text
        'RESET': ESC + '0m',  # Reset
    }

    # Colored symbols for each log level
    SYMBOLS = {
        'DEBUG': ESC + '36m○\033[0m',  # Cyan circle
        'INFO': ESC + '34mi\033[0m',  # Blue “i”
        'SUCCESS': ESC + '32m✓\033[0m',  # Green check
        'WARNING': ESC + '33m△\033[0m',  # Yellow triangle
        'ERROR': ESC + '31m✕\033[0m',  # Red X
        'CRITICAL': ESC + '31m⊗\033[0m',  # Red circled times
    }

    # Fallback symbols without colors (for terminals that don't support colors)
    SYMBOLS_NO_COLOR = {
        'DEBUG': '○',  # Wrench
        'INFO': 'i',  # Check mark
        'SUCCESS': '✓',  # Check mark
        'WARNING': '△',  # Warning
        'ERROR': '✕',  # X mark
        'CRITICAL': '⊗',  # Siren
    }

    def __init__(
        self,
        include_colors: bool = True,
        include_module: bool = True,
        ultra_minimal: bool = False,
        include_symbols: bool = True,
    ):
        """
        Initialize the custom formatter.

        Args:
            include_colors (bool): Whether to include colors in console output
            include_module (bool): Whether to include module name in log format
            ultra_minimal (bool): Whether to use ultra minimal format (just message)
            include_symbols (bool): Whether to include colored symbols before messages
        """
        self.include_colors = include_colors and self._supports_color()
        self.include_module = include_module
        self.ultra_minimal = ultra_minimal
        self.include_symbols = include_symbols

        # Base format without colors
        if ultra_minimal:
            base_format = '%(message)s'
        else:
            base_format = '%(asctime)s'
            if include_module:
                base_format += ' | %(name)-20s'
            base_format += ' | %(levelname)-8s | %(message)s'

        super().__init__(base_format, datefmt='%H:%M:%S')

    def _supports_color(self) -> bool:
        """Check if the current terminal supports colors."""
        # Check if we're in a terminal that supports colors
        if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty():
            return False

        # Check environment variables
        if os.environ.get('NO_COLOR'):
            return False

        if os.environ.get('FORCE_COLOR'):
            return True

        # Check common terminals that support colors
        term = os.environ.get('TERM', '').lower()
        return 'color' in term or term in [
            'xterm',
            'xterm-256color',
            'screen',
            'tmux',
        ]

    def format(self, record: logging.LogRecord) -> str:
        """Format the log record with optional colors and symbols."""
        # In ultra-minimal mode, format with optional symbols
        if self.ultra_minimal:
            message = record.getMessage()
            if self.include_symbols:
                if self.include_colors:
                    symbol = self.SYMBOLS.get(record.levelname, '')
                else:
                    symbol = self.SYMBOLS_NO_COLOR.get(record.levelname, '')
                return f'{symbol} {message}'
            return message

        # Create a copy of the record to avoid modifying the original
        record_copy = logging.makeLogRecord(record.__dict__)

        # Add colors if enabled
        if self.include_colors:
            level_color = self.COLORS.get(
                record_copy.levelname, self.COLORS['RESET']
            )
            record_copy.levelname = (
                f"{level_color}{record_copy.levelname}{self.COLORS['RESET']}"
            )

            # Add color to module name if it's a pyfabricops module
            if (
                hasattr(record_copy, 'name')
                and 'pyfabricops' in record_copy.name
            ):
                record_copy.name = f'\033[34m{record_copy.name}\033[0m'  # Blue

        # Format the message with the base formatter
        formatted_message = super().format(record_copy)

        # Add colored symbol prefix if enabled
        if self.include_symbols:
            if self.include_colors:
                symbol = self.SYMBOLS.get(record.levelname, '')
            else:
                symbol = self.SYMBOLS_NO_COLOR.get(record.levelname, '')
            return f'{symbol} {formatted_message}'

        return formatted_message

__init__(include_colors=True, include_module=True, ultra_minimal=False, include_symbols=True)

Initialize the custom formatter.

Parameters:

Name Type Description Default
include_colors bool

Whether to include colors in console output

True
include_module bool

Whether to include module name in log format

True
ultra_minimal bool

Whether to use ultra minimal format (just message)

False
include_symbols bool

Whether to include colored symbols before messages

True
Source code in src/pyfabricops/utils/logging.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def __init__(
    self,
    include_colors: bool = True,
    include_module: bool = True,
    ultra_minimal: bool = False,
    include_symbols: bool = True,
):
    """
    Initialize the custom formatter.

    Args:
        include_colors (bool): Whether to include colors in console output
        include_module (bool): Whether to include module name in log format
        ultra_minimal (bool): Whether to use ultra minimal format (just message)
        include_symbols (bool): Whether to include colored symbols before messages
    """
    self.include_colors = include_colors and self._supports_color()
    self.include_module = include_module
    self.ultra_minimal = ultra_minimal
    self.include_symbols = include_symbols

    # Base format without colors
    if ultra_minimal:
        base_format = '%(message)s'
    else:
        base_format = '%(asctime)s'
        if include_module:
            base_format += ' | %(name)-20s'
        base_format += ' | %(levelname)-8s | %(message)s'

    super().__init__(base_format, datefmt='%H:%M:%S')

format(record)

Format the log record with optional colors and symbols.

Source code in src/pyfabricops/utils/logging.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def format(self, record: logging.LogRecord) -> str:
    """Format the log record with optional colors and symbols."""
    # In ultra-minimal mode, format with optional symbols
    if self.ultra_minimal:
        message = record.getMessage()
        if self.include_symbols:
            if self.include_colors:
                symbol = self.SYMBOLS.get(record.levelname, '')
            else:
                symbol = self.SYMBOLS_NO_COLOR.get(record.levelname, '')
            return f'{symbol} {message}'
        return message

    # Create a copy of the record to avoid modifying the original
    record_copy = logging.makeLogRecord(record.__dict__)

    # Add colors if enabled
    if self.include_colors:
        level_color = self.COLORS.get(
            record_copy.levelname, self.COLORS['RESET']
        )
        record_copy.levelname = (
            f"{level_color}{record_copy.levelname}{self.COLORS['RESET']}"
        )

        # Add color to module name if it's a pyfabricops module
        if (
            hasattr(record_copy, 'name')
            and 'pyfabricops' in record_copy.name
        ):
            record_copy.name = f'\033[34m{record_copy.name}\033[0m'  # Blue

    # Format the message with the base formatter
    formatted_message = super().format(record_copy)

    # Add colored symbol prefix if enabled
    if self.include_symbols:
        if self.include_colors:
            symbol = self.SYMBOLS.get(record.levelname, '')
        else:
            symbol = self.SYMBOLS_NO_COLOR.get(record.levelname, '')
        return f'{symbol} {formatted_message}'

    return formatted_message

disable_logging()

Disable all logging output.

Source code in src/pyfabricops/utils/logging.py
354
355
356
def disable_logging() -> None:
    """Disable all logging output."""
    logging.getLogger('pyfabricops').setLevel(logging.CRITICAL + 1)

enable_debug_mode(include_external=True)

Quick function to enable debug logging with detailed output.

Parameters:

Name Type Description Default
include_external bool

Whether to include logs from external libraries

True
Source code in src/pyfabricops/utils/logging.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
def enable_debug_mode(include_external: bool = True) -> None:
    """
    Quick function to enable debug logging with detailed output.

    Args:
        include_external: Whether to include logs from external libraries
    """
    setup_logging(
        level=logging.DEBUG,
        format_style='detailed',
        include_colors=True,
        include_module=True,
        include_external=include_external,
    )

get_logger(name)

Get a logger instance for a specific module.

Parameters:

Name Type Description Default
name str

The logger name (usually name)

required

Returns:

Type Description
Logger

logging.Logger: Configured logger instance

Source code in src/pyfabricops/utils/logging.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def get_logger(name: str) -> logging.Logger:
    """
    Get a logger instance for a specific module.

    Args:
        name: The logger name (usually __name__)

    Returns:
        logging.Logger: Configured logger instance
    """
    # Ensure the name starts with pyfabricops
    if not name.startswith('pyfabricops'):
        if name == '__main__':
            name = 'pyfabricops.main'
        else:
            name = f'pyfabricops.{name}'

    logger = logging.getLogger(name)

    # Add NullHandler as fallback if no configuration is set
    if not logger.handlers and not logger.parent.handlers:
        logger.addHandler(logging.NullHandler())

    return logger

reset_logging()

Reset logging to default configuration.

Source code in src/pyfabricops/utils/logging.py
359
360
361
362
363
364
365
366
367
368
369
def reset_logging() -> None:
    """Reset logging to default configuration."""
    root_logger = logging.getLogger('pyfabricops')

    # Clear all handlers
    for handler in root_logger.handlers[:]:
        root_logger.removeHandler(handler)

    # Add back the null handler
    root_logger.addHandler(logging.NullHandler())
    root_logger.setLevel(logging.WARNING)

setup_logging(level=logging.INFO, format_style='standard', include_colors=True, include_module=True, include_symbols=True, log_file=None, include_external=False, max_file_size=10 * 1024 * 1024, backup_count=5)

Configure logging for pyfabricops.

Parameters:

Name Type Description Default
level Union[str, int]

Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL or numeric)

INFO
format_style Literal['standard', 'minimal', 'detailed']

Format style ('standard', 'minimal', 'detailed') - 'minimal': Only message (ultra-minimal) - 'standard': Timestamp, level, and message - 'detailed': Timestamp, module, level, and message

'standard'
include_colors bool

Whether to use colored output in console

True
include_module bool

Whether to include module names in logs

True
include_symbols bool

Whether to include colored symbols before messages (enabled by default)

True
log_file Optional[Union[str, Path]]

Optional file path to write logs to

None
include_external bool

Whether to include logs from external libraries

False
max_file_size int

Maximum size of log file before rotation (bytes)

10 * 1024 * 1024
backup_count int

Number of backup files to keep when rotating

5
Source code in src/pyfabricops/utils/logging.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def setup_logging(
    level: Union[str, int] = logging.INFO,
    format_style: Literal['standard', 'minimal', 'detailed'] = 'standard',
    include_colors: bool = True,
    include_module: bool = True,
    include_symbols: bool = True,
    log_file: Optional[Union[str, Path]] = None,
    include_external: bool = False,
    max_file_size: int = 10 * 1024 * 1024,  # 10MB
    backup_count: int = 5,
) -> None:
    """
    Configure logging for pyfabricops.

    Args:
        level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL or numeric)
        format_style: Format style ('standard', 'minimal', 'detailed')
                     - 'minimal': Only message (ultra-minimal)
                     - 'standard': Timestamp, level, and message
                     - 'detailed': Timestamp, module, level, and message
        include_colors: Whether to use colored output in console
        include_module: Whether to include module names in logs
        include_symbols: Whether to include colored symbols before messages (enabled by default)
        log_file: Optional file path to write logs to
        include_external: Whether to include logs from external libraries
        max_file_size: Maximum size of log file before rotation (bytes)
        backup_count: Number of backup files to keep when rotating
    """
    # Convert string level to numeric if needed
    if isinstance(level, str):
        level = getattr(logging, level.upper(), logging.INFO)

    # Get the root logger for pyfabricops
    root_logger = logging.getLogger('pyfabricops')

    # Clear existing handlers to avoid duplicates
    for handler in root_logger.handlers[:]:
        root_logger.removeHandler(handler)

    # Set the logging level
    root_logger.setLevel(level)

    # Create console handler
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(level)

    # Create formatter based on style
    if format_style == 'minimal':
        formatter = PyFabricOpsFormatter(
            include_colors=include_colors,
            include_module=False,
            ultra_minimal=True,
            include_symbols=include_symbols,
        )
    elif format_style == 'detailed':
        formatter = PyFabricOpsFormatter(
            include_colors=include_colors,
            include_module=True,
            ultra_minimal=False,
            include_symbols=include_symbols,
        )
    else:  # standard
        formatter = PyFabricOpsFormatter(
            include_colors=include_colors,
            include_module=include_module,
            ultra_minimal=False,
            include_symbols=include_symbols,
        )

    console_handler.setFormatter(formatter)

    # Add filter
    console_filter = PyFabricOpsFilter(include_external=include_external)
    console_handler.addFilter(console_filter)

    # Add console handler
    root_logger.addHandler(console_handler)

    # Add file handler if specified
    if log_file:
        log_path = Path(log_file)
        log_path.parent.mkdir(parents=True, exist_ok=True)

        # Use rotating file handler to prevent huge log files
        file_handler = logging.handlers.RotatingFileHandler(
            log_path,
            maxBytes=max_file_size,
            backupCount=backup_count,
            encoding='utf-8',
        )
        file_handler.setLevel(level)

        # File logs don't need colors or symbols (for better readability in files)
        file_formatter = PyFabricOpsFormatter(
            include_colors=False, include_module=True, include_symbols=False
        )
        file_handler.setFormatter(file_formatter)
        file_handler.addFilter(console_filter)

        root_logger.addHandler(file_handler)

    # Prevent propagation to root logger to avoid duplicate messages
    root_logger.propagate = False

success(self, message, *args, **kwargs)

Log a message with severity 'SUCCESS'.

Source code in src/pyfabricops/utils/logging.py
39
40
41
42
def success(self, message, *args, **kwargs):
    """Log a message with severity 'SUCCESS'."""
    if self.isEnabledFor(SUCCESS_LEVEL):
        self._log(SUCCESS_LEVEL, message, args, **kwargs)