mirror of
https://github.com/duthaho/claudekit.git
synced 2026-06-11 04:34:56 +03:00
feat: improved the Claude Kit as a plugin
This commit is contained in:
@@ -0,0 +1,506 @@
|
||||
# Logging — Python Patterns (structlog)
|
||||
|
||||
Reference examples for the [logging skill](./SKILL.md). All patterns use [structlog](https://www.structlog.org/) with stdlib integration.
|
||||
|
||||
---
|
||||
|
||||
## 1. Structured Logging Setup
|
||||
|
||||
Configure structured logging once at application startup. All modules then use `structlog.get_logger(__name__)`.
|
||||
|
||||
```python
|
||||
# logging_config.py
|
||||
import logging
|
||||
import structlog
|
||||
|
||||
def configure_logging(log_level: str = "INFO", json_output: bool = True) -> None:
|
||||
"""Configure structured logging for the application.
|
||||
|
||||
Call this once at application startup, before any loggers are created.
|
||||
"""
|
||||
# Set the stdlib logging level as the baseline filter
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
level=getattr(logging, log_level.upper()),
|
||||
)
|
||||
|
||||
# Choose renderers based on environment
|
||||
if json_output:
|
||||
renderer = structlog.processors.JSONRenderer()
|
||||
else:
|
||||
# Human-readable output for local development
|
||||
renderer = structlog.dev.ConsoleRenderer(colors=True)
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
structlog.processors.UnicodeDecoder(),
|
||||
renderer,
|
||||
],
|
||||
context_class=dict,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
# Usage anywhere in the application
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
async def create_user(email: str) -> User:
|
||||
logger.info("creating_user", email=email)
|
||||
user = await user_repo.create(email=email)
|
||||
logger.info("user_created", user_id=user.id, email=email)
|
||||
return user
|
||||
```
|
||||
|
||||
**Output (JSON mode):**
|
||||
```json
|
||||
{"event": "user_created", "user_id": 42, "email": "alice@example.com", "logger": "app.services.user", "level": "info", "timestamp": "2025-06-15T10:30:00.123Z"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Log Levels
|
||||
|
||||
```python
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# DEBUG: Detailed internals for troubleshooting
|
||||
logger.debug("cache_lookup", key="user:42", hit=True, ttl_remaining=120)
|
||||
|
||||
# INFO: Normal business events
|
||||
logger.info("order_placed", order_id="ORD-123", total=99.99, items=3)
|
||||
|
||||
# WARNING: Degraded but functional
|
||||
logger.warning(
|
||||
"rate_limit_approaching",
|
||||
current_rate=450,
|
||||
limit=500,
|
||||
window_seconds=60,
|
||||
)
|
||||
|
||||
# ERROR: Operation failed, needs attention
|
||||
logger.error(
|
||||
"payment_failed",
|
||||
order_id="ORD-123",
|
||||
provider="stripe",
|
||||
error_code="card_declined",
|
||||
exc_info=True, # Include stack trace
|
||||
)
|
||||
|
||||
# CRITICAL: System-level failure
|
||||
logger.critical(
|
||||
"database_pool_exhausted",
|
||||
active_connections=100,
|
||||
max_connections=100,
|
||||
waiting_requests=47,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Correlation IDs
|
||||
|
||||
Correlation IDs tie together all log entries from a single request. Uses FastAPI middleware with `contextvars`.
|
||||
|
||||
```python
|
||||
# middleware/correlation.py
|
||||
import uuid
|
||||
from contextvars import ContextVar
|
||||
|
||||
import structlog
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
|
||||
# Context variable accessible from any async task in the same request
|
||||
correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
|
||||
|
||||
class CorrelationIDMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# Accept an incoming correlation ID or generate a new one
|
||||
correlation_id = request.headers.get("X-Correlation-ID", uuid.uuid4().hex)
|
||||
correlation_id_var.set(correlation_id)
|
||||
|
||||
# Bind to structlog context so all logs in this request include it
|
||||
structlog.contextvars.clear_contextvars()
|
||||
structlog.contextvars.bind_contextvars(correlation_id=correlation_id)
|
||||
|
||||
response = await call_next(request)
|
||||
response.headers["X-Correlation-ID"] = correlation_id
|
||||
return response
|
||||
```
|
||||
|
||||
```python
|
||||
# Register the middleware
|
||||
from middleware.correlation import CorrelationIDMiddleware
|
||||
|
||||
app.add_middleware(CorrelationIDMiddleware)
|
||||
```
|
||||
|
||||
```python
|
||||
# Any logger call in any module now includes correlation_id automatically
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
async def get_user(user_id: int) -> User:
|
||||
logger.info("fetching_user", user_id=user_id)
|
||||
# Output: {"event": "fetching_user", "user_id": 42, "correlation_id": "a1b2c3d4...", ...}
|
||||
return await user_repo.get(user_id)
|
||||
```
|
||||
|
||||
### Propagating to downstream services
|
||||
|
||||
When calling other microservices, forward the correlation ID:
|
||||
|
||||
```python
|
||||
# Python — httpx client
|
||||
import httpx
|
||||
from middleware.correlation import correlation_id_var
|
||||
|
||||
async def call_billing_service(user_id: int) -> dict:
|
||||
correlation_id = correlation_id_var.get()
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"http://billing-service/api/v1/invoices?user_id={user_id}",
|
||||
headers={"X-Correlation-ID": correlation_id},
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Sensitive Data Redaction
|
||||
|
||||
Build redaction into the logging pipeline as a structlog processor so developers cannot accidentally leak secrets.
|
||||
|
||||
```python
|
||||
# processors/redact.py
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
# Patterns for sensitive field names (case-insensitive matching)
|
||||
SENSITIVE_KEYS = re.compile(
|
||||
r"(password|passwd|secret|token|api_key|apikey|authorization|"
|
||||
r"credit_card|card_number|cvv|ssn|social_security)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
# Pattern for credit card numbers in string values
|
||||
CREDIT_CARD_PATTERN = re.compile(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b")
|
||||
|
||||
# Pattern for bearer tokens in string values
|
||||
BEARER_PATTERN = re.compile(r"Bearer\s+[A-Za-z0-9\-._~+/]+=*", re.IGNORECASE)
|
||||
|
||||
|
||||
def redact_sensitive_data(
|
||||
logger: Any, method_name: str, event_dict: dict
|
||||
) -> dict:
|
||||
"""Structlog processor that masks sensitive values."""
|
||||
return _redact_dict(event_dict)
|
||||
|
||||
|
||||
def _redact_dict(data: dict) -> dict:
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
if SENSITIVE_KEYS.search(key):
|
||||
result[key] = "***REDACTED***"
|
||||
elif isinstance(value, dict):
|
||||
result[key] = _redact_dict(value)
|
||||
elif isinstance(value, str):
|
||||
result[key] = _redact_string(value)
|
||||
elif isinstance(value, list):
|
||||
result[key] = [
|
||||
_redact_dict(item) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
def _redact_string(value: str) -> str:
|
||||
value = CREDIT_CARD_PATTERN.sub("****-****-****-****", value)
|
||||
value = BEARER_PATTERN.sub("Bearer ***REDACTED***", value)
|
||||
return value
|
||||
```
|
||||
|
||||
```python
|
||||
# Add the processor to structlog configuration
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
redact_sensitive_data, # Add before the renderer
|
||||
structlog.processors.JSONRenderer(),
|
||||
],
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Request/Response Logging
|
||||
|
||||
Log every HTTP request and response with method, path, status code, duration, and body size. Uses FastAPI middleware.
|
||||
|
||||
```python
|
||||
# middleware/request_logging.py
|
||||
import time
|
||||
import structlog
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
|
||||
logger = structlog.get_logger("http")
|
||||
|
||||
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# Log request
|
||||
logger.info(
|
||||
"http_request_started",
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
query=str(request.url.query) or None,
|
||||
client_ip=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
except Exception:
|
||||
duration_ms = (time.perf_counter() - start_time) * 1000
|
||||
logger.error(
|
||||
"http_request_failed",
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
duration_ms=round(duration_ms, 2),
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
duration_ms = (time.perf_counter() - start_time) * 1000
|
||||
content_length = response.headers.get("content-length")
|
||||
|
||||
# Choose log level based on status code
|
||||
log_method = logger.info
|
||||
if response.status_code >= 500:
|
||||
log_method = logger.error
|
||||
elif response.status_code >= 400:
|
||||
log_method = logger.warning
|
||||
|
||||
log_method(
|
||||
"http_request_completed",
|
||||
method=request.method,
|
||||
path=request.url.path,
|
||||
status_code=response.status_code,
|
||||
duration_ms=round(duration_ms, 2),
|
||||
content_length=int(content_length) if content_length else None,
|
||||
)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Error Logging
|
||||
|
||||
When logging errors, include the stack trace, relevant context, and enough information to reproduce the issue.
|
||||
|
||||
```python
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
async def process_order(order_id: str) -> Order:
|
||||
logger.info("processing_order", order_id=order_id)
|
||||
|
||||
try:
|
||||
order = await order_repo.get(order_id)
|
||||
if not order:
|
||||
logger.error("order_not_found", order_id=order_id)
|
||||
raise OrderNotFoundError(order_id)
|
||||
|
||||
payment = await payment_service.charge(
|
||||
amount=order.total,
|
||||
currency=order.currency,
|
||||
customer_id=order.customer_id,
|
||||
)
|
||||
logger.info(
|
||||
"payment_processed",
|
||||
order_id=order_id,
|
||||
payment_id=payment.id,
|
||||
amount=order.total,
|
||||
)
|
||||
|
||||
except PaymentError as exc:
|
||||
# Log the error with full context for debugging
|
||||
logger.error(
|
||||
"payment_failed",
|
||||
order_id=order_id,
|
||||
customer_id=order.customer_id,
|
||||
amount=order.total,
|
||||
error_code=exc.code,
|
||||
error_message=str(exc),
|
||||
exc_info=True, # Includes full stack trace
|
||||
)
|
||||
raise
|
||||
|
||||
except Exception as exc:
|
||||
# Catch-all for unexpected errors
|
||||
logger.exception(
|
||||
"order_processing_unexpected_error",
|
||||
order_id=order_id,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
raise
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Performance Logging
|
||||
|
||||
### Timing decorator
|
||||
|
||||
```python
|
||||
import functools
|
||||
import time
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger("performance")
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
def log_duration(operation: str, slow_threshold_ms: float = 1000.0):
|
||||
"""Decorator that logs the duration of a function call.
|
||||
|
||||
Args:
|
||||
operation: A descriptive name for the operation.
|
||||
slow_threshold_ms: Threshold in milliseconds above which
|
||||
the log level escalates to WARNING.
|
||||
"""
|
||||
def decorator(func: F) -> F:
|
||||
@functools.wraps(func)
|
||||
async def async_wrapper(*args, **kwargs):
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
return result
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
log_fn = (
|
||||
logger.warning
|
||||
if duration_ms > slow_threshold_ms
|
||||
else logger.debug
|
||||
)
|
||||
log_fn(
|
||||
"operation_duration",
|
||||
operation=operation,
|
||||
duration_ms=round(duration_ms, 2),
|
||||
slow=duration_ms > slow_threshold_ms,
|
||||
)
|
||||
|
||||
@functools.wraps(func)
|
||||
def sync_wrapper(*args, **kwargs):
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
finally:
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
log_fn = (
|
||||
logger.warning
|
||||
if duration_ms > slow_threshold_ms
|
||||
else logger.debug
|
||||
)
|
||||
log_fn(
|
||||
"operation_duration",
|
||||
operation=operation,
|
||||
duration_ms=round(duration_ms, 2),
|
||||
slow=duration_ms > slow_threshold_ms,
|
||||
)
|
||||
|
||||
import asyncio
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
return async_wrapper # type: ignore
|
||||
return sync_wrapper # type: ignore
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# Usage
|
||||
@log_duration("fetch_user_profile", slow_threshold_ms=200)
|
||||
async def get_user_profile(user_id: int) -> UserProfile:
|
||||
return await user_repo.get_with_preferences(user_id)
|
||||
```
|
||||
|
||||
### Context manager for ad-hoc timing
|
||||
|
||||
```python
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger("performance")
|
||||
|
||||
@contextmanager
|
||||
def log_timing(operation: str, **extra_fields):
|
||||
"""Context manager for timing arbitrary code blocks."""
|
||||
start = time.perf_counter()
|
||||
yield
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
logger.info(
|
||||
"operation_duration",
|
||||
operation=operation,
|
||||
duration_ms=round(duration_ms, 2),
|
||||
**extra_fields,
|
||||
)
|
||||
|
||||
# Usage
|
||||
async def rebuild_search_index():
|
||||
with log_timing("rebuild_search_index", index="products"):
|
||||
products = await product_repo.get_all()
|
||||
await search_service.reindex(products)
|
||||
```
|
||||
|
||||
### Slow query logging (SQLAlchemy)
|
||||
|
||||
```python
|
||||
# SQLAlchemy event listener for slow queries
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger("database")
|
||||
|
||||
SLOW_QUERY_THRESHOLD_MS = 500
|
||||
|
||||
@event.listens_for(Engine, "before_cursor_execute")
|
||||
def before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
||||
conn.info.setdefault("query_start_time", []).append(time.perf_counter())
|
||||
|
||||
@event.listens_for(Engine, "after_cursor_execute")
|
||||
def after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
||||
total_ms = (time.perf_counter() - conn.info["query_start_time"].pop()) * 1000
|
||||
if total_ms > SLOW_QUERY_THRESHOLD_MS:
|
||||
logger.warning(
|
||||
"slow_query",
|
||||
duration_ms=round(total_ms, 2),
|
||||
statement=statement[:500], # Truncate long queries
|
||||
parameters=str(parameters)[:200],
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user