Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions codeflow_engine/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
"""Utility modules for CodeFlow Engine."""
"""
Utility modules for CodeFlow Engine.

Note: Core portable utilities (logging, resilience) are also available
in the standalone codeflow-utils package at tools/packages/codeflow-utils-python.
This module re-exports engine-specific versions that integrate with CodeFlowSettings.
"""

from codeflow_engine.utils.logging import get_logger, log_with_context, setup_logging
from codeflow_engine.utils.resilience import (
CircuitBreaker,
CircuitBreakerOpenError,
CircuitBreakerState,
)

__all__ = ["get_logger", "log_with_context", "setup_logging"]
__all__ = [
"get_logger",
"log_with_context",
"setup_logging",
"CircuitBreaker",
"CircuitBreakerOpenError",
"CircuitBreakerState",
]
57 changes: 57 additions & 0 deletions tools/packages/codeflow-utils-python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "codeflow-utils"
version = "0.1.0"
description = "Shared utility functions for CodeFlow Python applications"
readme = "README.md"
license = "MIT"
requires-python = ">=3.12"
keywords = ["codeflow", "utils", "utilities", "resilience", "logging"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]

dependencies = []

[project.optional-dependencies]
logging = ["structlog>=24.0.0"]
resilience = []
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.24.0",
"mypy>=1.11.0",
"ruff>=0.6.0",
]

[project.urls]
Documentation = "https://github.com/JustAGhosT/codeflow-engine/tree/main/tools/packages/codeflow-utils-python"
Source = "https://github.com/JustAGhosT/codeflow-engine"

[tool.hatch.build.targets.sdist]
include = [
"/src",
]

[tool.hatch.build.targets.wheel]
packages = ["src/codeflow_utils"]

[tool.mypy]
python_version = "3.12"
strict = true

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "B", "UP"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
codeflow-utils - Shared utility functions for CodeFlow Python applications.

This package provides portable utilities that can be used across CodeFlow projects
without tight coupling to any specific application.

Modules:
logging: Structured logging utilities
resilience: Circuit breaker and retry patterns
"""

from codeflow_utils.logging import (
JsonFormatter,
TextFormatter,
get_logger,
log_with_context,
)
from codeflow_utils.resilience import (
CircuitBreaker,
CircuitBreakerConfig,
CircuitBreakerOpenError,
CircuitBreakerState,
CircuitBreakerStats,
)

__version__ = "0.1.0"

__all__ = [
# Logging
"JsonFormatter",
"TextFormatter",
"get_logger",
"log_with_context",
# Resilience
"CircuitBreaker",
"CircuitBreakerConfig",
"CircuitBreakerOpenError",
"CircuitBreakerState",
"CircuitBreakerStats",
]
216 changes: 216 additions & 0 deletions tools/packages/codeflow-utils-python/src/codeflow_utils/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
"""
Structured logging utilities for CodeFlow applications.

Provides JSON-formatted structured logging with correlation IDs and
contextual information. This is a portable version that doesn't depend
on codeflow_engine internals.
"""

import json
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


class JsonFormatter(logging.Formatter):
"""JSON formatter for structured logging."""

def __init__(self, service_name: str = "codeflow"):
"""
Initialize the JSON formatter.

Args:
service_name: Name of the service for log identification
"""
super().__init__()
self.service_name = service_name

def format(self, record: logging.LogRecord) -> str:
"""
Format log record as JSON.

Args:
record: Log record to format

Returns:
JSON-formatted log string
"""
log_data: dict[str, Any] = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": record.levelname,
"service": self.service_name,
"component": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}

# Add extra fields from record
if hasattr(record, "request_id"):
log_data["request_id"] = record.request_id
if hasattr(record, "user_id"):
log_data["user_id"] = record.user_id
if hasattr(record, "duration_ms"):
log_data["duration_ms"] = record.duration_ms
if hasattr(record, "correlation_id"):
log_data["correlation_id"] = record.correlation_id

# Add exception info if present
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
log_data["exception_type"] = (
record.exc_info[0].__name__ if record.exc_info[0] else None
)

# Add any additional extra fields
excluded_keys = {
"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 not in excluded_keys and not key.startswith("_"):
log_data[key] = value

return json.dumps(log_data, default=str)


class TextFormatter(logging.Formatter):
"""Human-readable text formatter for development."""

def format(self, record: logging.LogRecord) -> str:
"""
Format log record as human-readable text.

Args:
record: Log record to format

Returns:
Formatted log string
"""
timestamp = datetime.fromtimestamp(record.created, tz=timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S UTC"
)
level = record.levelname.ljust(8)
component = record.name
message = record.getMessage()

# Build context string
context_parts = []
if hasattr(record, "request_id"):
context_parts.append(f"request_id={record.request_id}")
if hasattr(record, "user_id"):
context_parts.append(f"user_id={record.user_id}")
if hasattr(record, "duration_ms"):
context_parts.append(f"duration={record.duration_ms}ms")

context = f" [{', '.join(context_parts)}]" if context_parts else ""

log_line = f"{timestamp} | {level} | {component} | {message}{context}"

# Add exception if present
if record.exc_info:
log_line += f"\n{self.formatException(record.exc_info)}"

return log_line


def setup_logging(
level: str = "INFO",
format_type: str = "json",
output: str = "stdout",
log_file: str | Path | None = None,
service_name: str = "codeflow",
) -> None:
"""
Configure structured logging.

Args:
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
format_type: Format type ('json' or 'text')
output: Output destination ('stdout', 'file', or 'both')
log_file: Path to log file (required if output is 'file' or 'both')
service_name: Name of the service for log identification
"""
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, level.upper(), logging.INFO))
root_logger.handlers.clear()

if format_type == "json":
formatter = JsonFormatter(service_name=service_name)
else:
formatter = TextFormatter()

if output in ("stdout", "both"):
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
stdout_handler.setLevel(getattr(logging, level.upper(), logging.INFO))
root_logger.addHandler(stdout_handler)

if output in ("file", "both"):
if log_file is None:
raise ValueError("log_file is required when output is 'file' or 'both'")
log_path = Path(log_file)
log_path.parent.mkdir(parents=True, exist_ok=True)

file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
file_handler.setLevel(getattr(logging, level.upper(), logging.INFO))
root_logger.addHandler(file_handler)

# Reduce noise from third-party libraries
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.WARNING)


def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance for a module.

Args:
name: Logger name (typically __name__)

Returns:
Logger instance
"""
return logging.getLogger(name)


def log_with_context(
logger: logging.Logger,
level: int,
message: str,
**context: Any,
) -> None:
"""
Log a message with additional context.

Args:
logger: Logger instance
level: Log level (logging.INFO, etc.)
message: Log message
**context: Additional context fields
"""
logger.log(level, message, extra=context)
Empty file.
Loading