Arlogi Architecture Documentation
This document describes the architecture, design patterns, and internal structure of the arlogi logging library.
Table of Contents
System Overview
Arlogi is a Python logging library built on top of the standard logging module. It provides:
- Custom TRACE level (below DEBUG) for ultra-detailed logging
- Caller attribution via stack frame inspection
- Multiple output handlers: Rich console, JSON files, Syslog
- Type-safe configuration via dataclasses
- Factory pattern for handler creation
Technology Stack
| Component | Technology | Purpose |
|---|---|---|
| Core Logging | logging module |
Python standard library foundation |
| Console Output | rich |
Premium colored terminal output |
| Type Safety | typing.Protocol |
Runtime-checkable type hints |
| Configuration | dataclasses |
Immutable configuration objects |
| Structured Logs | json |
Machine-readable log output |
Architecture Diagrams
C4 Context Diagram
graph TB
subgraph "Your Application"
App[Application Code]
end
subgraph "arlogi"
subgraph "arlogi"
API[Public API<br/>LoggingConfig, get_logger]
Logger[LoggerFactory<br/>TraceLogger]
Handlers[Handlers<br/>Console, JSON, Syslog]
end
subgraph "External Systems"
Console[Terminal]
Files[Log Files]
Syslog[Syslog Server]
end
App --> API
API --> Logger
Logger --> Handlers
Handlers --> Console
Handlers --> Files
Handlers --> Syslog
C4 Container Diagram
graph TB
subgraph "arlogi Library"
subgraph "Public API Layer"
Init[__init__.py<br/>Public Exports]
Factory[factory.py<br/>LoggerFactory]
end
subgraph "Configuration Layer"
Config[config.py<br/>LoggingConfig]
HF[handler_factory.py<br/>HandlerFactory]
end
subgraph "Core Layer"
Levels[levels.py<br/>TRACE Registration]
Types[types.py<br/>LoggerProtocol]
TraceLog[TraceLogger<br/>Caller Attribution]
end
subgraph "Handlers Layer"
Console[ColoredConsoleHandler<br/>Rich Output]
JSON[JSONHandler<br/>Structured Logs]
SyslogHandler[ArlogiSyslogHandler<br/>Syslog Output]
end
end
Init --> Factory
Factory --> Config
Factory --> HF
Factory --> TraceLog
HF --> Console
HF --> JSON
HF --> SyslogHandler
TraceLog --> Types
TraceLog --> Levels
Component Dependency Diagram
graph LR
subgraph "Public Module"
Init[__init__.py]
end
subgraph "Core Modules"
Factory[factory.py]
Config[config.py]
HandlerFac[handler_factory.py]
Handlers[handlers.py]
Levels[levels.py]
Types[types.py]
end
Init --> Factory
Init --> Config
Init --> HandlerFac
Init --> Levels
Init --> Types
Factory --> Config
Factory --> HandlerFac
Factory --> Handlers
Factory --> Levels
HandlerFac --> Handlers
Sequence Diagram: Logging Flow
sequenceDiagram
participant App as Application
participant Config as LoggingConfig
participant Factory as LoggerFactory
participant Logger as TraceLogger
participant Handler as ColoredConsoleHandler
participant Rich as RichHandler
participant Console as Terminal
App->>Config: LoggingConfig(level="INFO")
App->>Factory: _apply_configuration(config)
Factory->>Logger: Root Logger level set
Factory->>Handler: Handlers created/added
App->>Logger: logger.info("msg", caller_depth=1)
Logger->>Logger: _process_params (attribution)
Logger->>Handler: emit(record)
Handler->>Rich: _log_render(...)
Rich->>Console: Print formatted output
Class Diagram
classDiagram
class LoggingConfig {
<<frozen dataclass>>
+int|str level
+dict module_levels
+str json_file_name
+bool json_file_only
+bool use_syslog
+str|tuple syslog_address
+bool show_time
+bool show_level
+bool show_path
+resolved_level() int
+show_console() bool
+has_json_output() bool
+from_kwargs(**kwargs) LoggingConfig
+to_dict() dict
}
class HandlerFactory {
<<factory>>
+create_console(config) ColoredConsoleHandler
+create_json_stream() JSONHandler
+create_json_file(config) JSONFileHandler
+create_json_handler(config) Handler
+create_syslog(config) ArlogiSyslogHandler
+create_handlers(config) List~Handler~
}
class LoggerFactory {
<<factory>>
_initialized: bool
_global_logger: TraceLogger|None
+_apply_configuration(config) None
+get_logger(name, level) LoggerProtocol
+get_json_logger(name, file) LoggerProtocol
+get_syslog_logger(name, addr) LoggerProtocol
+cleanup_json_logger(name) None
+cleanup_syslog_logger(name) None
+is_test_mode() bool
-_initialize_trace_level() None
-_configure_root_logger(config) None
-_clear_and_add_handlers(config) None
-_configure_module_levels(config) None
}
class TraceLogger {
extends Logger
+_get_caller_info(depth) tuple
+_process_params(msg, kwargs) tuple
+trace(msg, *args, **kwargs) None
+debug(msg, *args, **kwargs) None
+info(msg, *args, **kwargs) None
+warning(msg, *args, **kwargs) None
+error(msg, *args, **kwargs) None
+critical(msg, *args, **kwargs) None
+exception(msg, *args, **kwargs) None
+log(level, msg, *args, **kwargs) None
}
class LoggerProtocol {
<<protocol>>
+trace(msg, *args, **kwargs) None
+debug(msg, *args, **kwargs) None
+info(msg, *args, **kwargs) None
+warning(msg, *args, **kwargs) None
+error(msg, *args, **kwargs) None
+critical(msg, *args, **kwargs) None
+exception(msg, *args, **kwargs) None
+log(level, msg, *args, **kwargs) None
+setLevel(level) None
+isEnabledFor(level) bool
+getEffectiveLevel() int
+name: str
}
class ColoredConsoleHandler {
extends RichHandler
-level_styles: dict
-project_root: str
+_find_project_root() str
+render(record, traceback, message) Any
+get_level_text(record) Any
+render_message(record, message) Any
}
class JSONHandler {
extends StreamHandler
}
class JSONFileHandler {
extends FileHandler
}
class ArlogiSyslogHandler {
extends SysLogHandler
}
LoggingConfig ..> HandlerFactory : uses
LoggingConfig ..> LoggerFactory : uses
LoggerFactory ..> HandlerFactory : uses
LoggerFactory ..> TraceLogger : creates
TraceLogger ..|> LoggerProtocol : implements
HandlerFactory ..> ColoredConsoleHandler : creates
HandlerFactory ..> JSONHandler : creates
HandlerFactory ..> JSONFileHandler : creates
HandlerFactory ..> ArlogiSyslogHandler : creates
Design Patterns
Factory Pattern
HandlerFactory encapsulates handler creation logic:
# Instead of direct instantiation
handler = ColoredConsoleHandler(show_time=True)
# Use factory for consistency and testability
handler = HandlerFactory.create_console(config)
Benefits:
- Single responsibility per factory method
- Easy to add new handler types
- Simplified testing with mock factories
Builder Pattern
LoggingConfig.from_kwargs() provides flexible configuration:
# Build configuration from multiple sources
config = LoggingConfig.from_kwargs(
level="INFO",
module_levels={"app.db": "DEBUG"}
)
LoggerFactory._apply_configuration(config)
Protocol Pattern
LoggerProtocol defines the logger interface:
@runtime_checkable
class LoggerProtocol(Protocol):
def info(self, msg: Any, *args: Any, **kwargs: Any) -> None: ...
Benefits:
- Type safety without inheritance
- Runtime checking with
isinstance() - Structural subtyping support
Strategy Pattern
Different handlers implement different output strategies:
# Console strategy
console = ColoredConsoleHandler()
# JSON strategy
json_handler = JSONHandler()
# Syslog strategy
syslog = ArlogiSyslogHandler()
Component Reference
Core Modules
| Module | Responsibility | Lines of Code |
|---|---|---|
factory.py |
Logger creation and configuration | ~450 |
handlers.py |
Output handler implementations | ~340 |
config.py |
Configuration dataclass | ~195 |
handler_factory.py |
Handler factory | ~170 |
levels.py |
TRACE level registration | ~20 |
types.py |
Logger protocol definition | ~25 |
File Structure
src/arlogi/
├── __init__.py # Public API exports
├── config.py # LoggingConfig dataclass
├── config_builder.py # Configuration builder utilities (if present)
├── factory.py # LoggerFactory, TraceLogger
├── handler_factory.py # HandlerFactory
├── handlers.py # All handler classes
├── levels.py # TRACE level registration
└── types.py # LoggerProtocol
Data Flow
Initialization Flow
The initialization process uses the LoggingConfig pattern for type-safe configuration:
graph TD
A[LoggingConfig init] --> B["LoggerFactory._apply_configuration"]
B --> C[_initialize_trace_level]
B --> D[_configure_root_logger]
B --> E[is_test_mode?]
E -->|No| F[_clear_and_add_handlers]
E -->|Yes| G[Skip - use pytest handlers]
F --> H[HandlerFactory.create_handlers]
H --> I[Add handlers to root]
B --> J[_configure_module_levels]
Logging Call Flow
graph TD
A[logger.info] --> B[_process_params]
B --> C{from_ set?}
C -->|Yes| D[_get_caller_info]
C -->|No| E[Skip attribution]
D --> F[Build attribution string]
F --> G[Append to message]
E --> G
G --> H["super().info"]
H --> I[Logging.Logger.info]
I --> J[Handler.emit]
J --> K{Handler Type}
K -->|Console| L[ColoredConsoleHandler]
K -->|JSON| M[JSONHandler]
K -->|Syslog| N[ArlogiSyslogHandler]
Extensibility
Adding Custom Handlers
from arlogi import HandlerFactory, LoggingConfig
from arlogi.handlers import ColoredConsoleHandler
class CustomConsoleHandler(ColoredConsoleHandler):
"""Custom handler with additional formatting."""
def emit(self, record):
# Custom pre-processing
record.custom_field = "custom_value"
super().emit(record)
# Extend HandlerFactory
class ExtendedHandlerFactory(HandlerFactory):
@staticmethod
def create_custom(config):
return CustomConsoleHandler(
show_time=config.show_time,
show_level=config.show_level
)
Adding Custom Log Levels
import logging
from arlogi.levels import TRACE_LEVEL_NUM
# Define a new level
VERBOSE = 8 # Between TRACE (5) and DEBUG (10)
# Register it
logging.addLevelName(VERBOSE, "VERBOSE")
setattr(logging, "VERBOSE", VERBOSE)
# Use it
logger.log(VERBOSE, "Verbose message")
Custom Configuration Sources
from arlogi import LoggingConfig
import yaml
def config_from_yaml(file_path):
"""Load LoggingConfig from YAML file."""
with open(file_path) as f:
data = yaml.safe_load(f)
return LoggingConfig(**data)
# Use it
config = config_from_yaml("logging_config.yaml")
Performance Considerations
Caller Attribution Overhead
| Operation | Time | Notes |
|---|---|---|
| Standard log call | ~0.5μs | No attribution |
Log with caller_depth= |
~1.5μs | Stack frame inspection |
| Deep stack (depth=5) | ~3μs | Multiple frame walks |
Optimization Tip: Use from_ only in development/debug builds.
Memory Usage
| Component | Memory | Notes |
|---|---|---|
| LoggingConfig | ~200 bytes | Immutable, shared |
| TraceLogger | ~1KB | Per logger instance |
| Handler instances | ~500 bytes each | Varies by handler type |
Error Handling Strategy
Graceful Degradation
graph TD
A[Logging Call] --> B{Handler Available?}
B -->|Yes| C[Emit to Handler]
B -->|No| D[Fallback to Stderr]
C --> E{Emit Success?}
E -->|Yes| F[Continue]
E -->|No| D
D --> F
Error Boundaries
| Component | Error Handling |
|---|---|
| LoggingConfig | Validates on init, raises ValueError |
| HandlerFactory | Raises ValueError for invalid config |
| LoggerFactory | Silently falls back on handler errors |
| ColoredConsoleHandler | Falls back to basic formatting |
| ArlogiSyslogHandler | Falls back to UDP, then silent |
Testing Strategy
Test Mode Detection
def is_test_mode() -> bool:
return (
"pytest" in sys.modules
or "unittest" in sys.modules
or os.environ.get("PYTEST_CURRENT_TEST") is not None
)
In test mode:
- Default level is DEBUG (not INFO)
- Handlers are NOT added to root (prevents double logging)
- Works seamlessly with
caplogfixture
Version Compatibility
| Python | arlogi | Status |
|---|---|---|
| 3.13+ | 0.601+ | Supported |
| 3.12 | 0.512+ | Supported (with uv) |
| 3.11 | 0.512+ | Supported (with uv) |
| <3.11 | - | Not supported |
Future Enhancements
Planned Features
- Async Handlers - AsyncIO-compatible log handlers
- Log Rotation - Built-in rotation for JSON files
- Filter Support - Per-handler log filtering
- Context Injection - Automatic request/context IDs
- Metrics Integration - OpenTelemetry integration
Extension Points
- Custom formatters via
Formattersubclassing - Custom filters via
Filtersubclassing - Custom handlers via
Handlersubclassing - Configuration plugins via
LoggingConfiginheritance