Source code for hfortix_core.logging.formatters
"""
Logging utilities for HFortix
Provides structured logging formatters and configuration helpers
for enterprise observability.
"""
from __future__ import annotations
import json
import logging
from datetime import datetime, timezone
from typing import Any
[docs]
class StructuredFormatter(logging.Formatter):
"""
Format log records as structured JSON
Useful for log aggregation systems like ELK, Splunk, CloudWatch
that can parse JSON logs for better searching and analysis.
Standard Fields (always present):
- timestamp: ISO 8601 UTC timestamp
- level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- logger: Logger name (e.g., "hfortix.http.client")
- message: Log message
Common Extra Fields (added via extra={...}):
- request_id: Unique request identifier for correlation
- method: HTTP method (GET, POST, PUT, DELETE, PATCH)
- endpoint: API endpoint path
- status_code: HTTP status code
- duration_seconds: Request duration in seconds
- vdom: FortiOS Virtual Domain (multi-tenant environments)
- adom: FortiManager/FortiAnalyzer Administrative Domain
- error_type: Exception class name (for errors)
- attempt: Current retry attempt number
- max_attempts: Maximum retry attempts
Example:
>>> import logging
>>> from hfortix_core.logging import StructuredFormatter
>>>
>>> handler = logging.StreamHandler()
>>> handler.setFormatter(StructuredFormatter())
>>> logger = logging.getLogger("hfortix")
>>> logger.addHandler(handler)
>>>
>>> # Basic usage
>>> logger.info("API request completed",
... extra={"endpoint": "/api/v2/cmdb/firewall/policy",
... "duration_seconds": 0.145})
Output:
{"timestamp":"2026-01-02T14:23:45.123Z","level":"INFO",
"logger":"hfortix","message":"API request completed",
"endpoint":"/api/v2/cmdb/firewall/policy","duration_seconds":0.145}
>>> # Multi-tenant usage
>>> logger.info("Policy created",
... extra={"vdom": "customer_a", "endpoint": "/api/v2/cmdb/firewall/policy", # noqa: E501
... "request_id": "req-123", "status_code": 200})
Output:
{"timestamp":"2026-01-02T14:23:45.456Z","level":"INFO",
"logger":"hfortix","message":"Policy created","vdom":"customer_a",
"endpoint":"/api/v2/cmdb/firewall/policy","request_id":"req-123",
"status_code":200}
"""
[docs]
def __init__(
self,
include_fields: list[str] | None = None,
exclude_fields: list[str] | None = None,
):
"""
Initialize structured formatter
Args:
include_fields: List of extra fields to include (None = all)
exclude_fields: List of fields to exclude from output
"""
super().__init__()
self.include_fields = include_fields
self.exclude_fields = exclude_fields or []
[docs]
def format(self, record: logging.LogRecord) -> str:
"""
Format log record as JSON
Args:
record: LogRecord to format
Returns:
JSON string with log data
"""
# Build base log structure
log_data: dict[str, Any] = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
# Add extra fields from record
if hasattr(record, "__dict__"):
# Get extra fields (those not in standard LogRecord)
standard_fields = {
"name",
"msg",
"args",
"created",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"message",
"pathname",
"process",
"processName",
"relativeCreated",
"thread",
"threadName",
"exc_info",
"exc_text",
"stack_info",
}
for key, value in record.__dict__.items():
if key in standard_fields:
continue
if key in self.exclude_fields:
continue
if (
self.include_fields is not None
and key not in self.include_fields
):
continue
# Add extra field
log_data[key] = value
# Add exception info if present
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
# Add source location for DEBUG level
if record.levelno <= logging.DEBUG:
log_data["source"] = {
"file": record.filename,
"line": record.lineno,
"function": record.funcName,
}
# Return compact JSON
return json.dumps(log_data, separators=(",", ":"), default=str)
[docs]
class TextFormatter(logging.Formatter):
"""
Format log records as human-readable text
Provides colorized output for terminal and clean formatting
for log files.
Example:
>>> import logging
>>> from hfortix_core.logging import TextFormatter
>>>
>>> handler = logging.StreamHandler()
>>> handler.setFormatter(TextFormatter(use_color=True))
>>> logger = logging.getLogger("hfortix")
>>> logger.addHandler(handler)
>>> logger.info("API request completed")
Output:
2026-01-02 14:23:45 [INFO] hfortix: API request completed
"""
# ANSI color codes
COLORS = {
"DEBUG": "\033[36m", # Cyan
"INFO": "\033[32m", # Green
"WARNING": "\033[33m", # Yellow
"ERROR": "\033[31m", # Red
"CRITICAL": "\033[35m", # Magenta
"RESET": "\033[0m", # Reset
}
[docs]
def __init__(self, use_color: bool = False):
"""
Initialize text formatter
Args:
use_color: Whether to use ANSI color codes
"""
super().__init__()
self.use_color = use_color
[docs]
def format(self, record: logging.LogRecord) -> str:
"""
Format log record as text
Args:
record: LogRecord to format
Returns:
Formatted text string
"""
# Format timestamp
timestamp = datetime.fromtimestamp(record.created).strftime(
"%Y-%m-%d %H:%M:%S"
)
# Format level with optional color
level = record.levelname
if self.use_color:
color = self.COLORS.get(level, "")
reset = self.COLORS["RESET"]
level = f"{color}{level}{reset}"
# Build message
message = f"{timestamp} [{level}] {record.name}: {record.getMessage()}"
# Add exception if present
if record.exc_info:
message += "\n" + self.formatException(record.exc_info)
return message
__all__ = ["StructuredFormatter", "TextFormatter"]