feat(core): 初始化核心配置和部署文件
- 添加 .env.example 环境变量配置示例 - 添加 .gitignore 忽略文件配置 - 添加 core/config.py 配置管理模块 - 添加 deployments/k8s/configmap.yaml Kubernetes 配置 - 添加 core/database.py 数据库连接管理模块 - 添加 core/dependencies.py 全局依赖模块 - 添加 DEPENDENCIES_UPDATED.md 依赖更新记录 - 添加 deployments/k8s/deployment.yaml Kubernetes 部署配置- 添加 deployments/swarm/docker-compose.swarm.yml Docker Swarm 部署配置 - 添加 deployments/docker/docker-compose.yml Docker 部署配置 - 添加 deployments/docker/Dockerfile 应用镜像构建文件 - 添加 middleware/error_handler.py 全局异常处理中间件
This commit is contained in:
0
observability/__init__.py
Normal file
0
observability/__init__.py
Normal file
187
observability/logging.py
Normal file
187
observability/logging.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Structured logging with trace context propagation.
|
||||
Integrates with OpenTelemetry for log correlation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
from typing import Any, MutableMapping
|
||||
from datetime import datetime
|
||||
from contextvars import ContextVar
|
||||
from core.config import settings
|
||||
|
||||
|
||||
# Context variable for trace ID
|
||||
trace_id_var: ContextVar[str] = ContextVar("trace_id", default="")
|
||||
|
||||
|
||||
class TraceContextFormatter(logging.Formatter):
|
||||
"""
|
||||
Custom formatter that adds trace context to log records.
|
||||
|
||||
Formats logs as JSON in production, human-readable in development.
|
||||
"""
|
||||
|
||||
def __init__(self, use_json: bool = True):
|
||||
"""
|
||||
Initialize formatter.
|
||||
|
||||
Args:
|
||||
use_json: Whether to format as JSON (True) or human-readable (False)
|
||||
"""
|
||||
super().__init__()
|
||||
self.use_json = use_json
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
"""
|
||||
Format log record with trace context.
|
||||
|
||||
Args:
|
||||
record: Log record
|
||||
|
||||
Returns:
|
||||
str: Formatted log message
|
||||
"""
|
||||
# Get trace ID from context
|
||||
trace_id = trace_id_var.get()
|
||||
|
||||
if self.use_json:
|
||||
# JSON format for production
|
||||
log_data = {
|
||||
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
"trace_id": trace_id,
|
||||
"module": record.module,
|
||||
"function": record.funcName,
|
||||
"line": record.lineno,
|
||||
}
|
||||
|
||||
# Add exception info if present
|
||||
if record.exc_info:
|
||||
log_data["exception"] = self.formatException(record.exc_info)
|
||||
|
||||
# Add extra fields
|
||||
if hasattr(record, "extra"):
|
||||
log_data["extra"] = getattr(record, "extra")
|
||||
|
||||
return json.dumps(log_data)
|
||||
else:
|
||||
# Human-readable format for development
|
||||
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
trace_part = f" [trace_id={trace_id}]" if trace_id else ""
|
||||
message = f"{timestamp} - {record.levelname:8s} - {record.name:30s}{trace_part} - {record.getMessage()}"
|
||||
|
||||
if record.exc_info:
|
||||
message += "\n" + self.formatException(record.exc_info)
|
||||
|
||||
return message
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
"""
|
||||
Configure application logging.
|
||||
|
||||
This should be called at application startup.
|
||||
"""
|
||||
# Determine if we should use JSON format
|
||||
use_json = settings.is_production
|
||||
|
||||
# Create formatter
|
||||
formatter = TraceContextFormatter(use_json=use_json)
|
||||
|
||||
# Configure root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(settings.log_level)
|
||||
|
||||
# Remove existing handlers
|
||||
root_logger.handlers.clear()
|
||||
|
||||
# Create console handler
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(settings.log_level)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
# Add handler to root logger
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# Set levels for third-party loggers
|
||||
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
||||
logging.getLogger("uvicorn.access").setLevel(logging.INFO)
|
||||
logging.getLogger("uvicorn.error").setLevel(logging.INFO)
|
||||
logging.getLogger("fastapi").setLevel(logging.INFO)
|
||||
|
||||
# Reduce noise from OpenTelemetry
|
||||
logging.getLogger("opentelemetry").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def set_trace_id(trace_id: str) -> None:
|
||||
"""
|
||||
Set trace ID in context for current request.
|
||||
|
||||
Args:
|
||||
trace_id: Trace ID to set
|
||||
"""
|
||||
trace_id_var.set(trace_id)
|
||||
|
||||
|
||||
def get_trace_id() -> str:
|
||||
"""
|
||||
Get trace ID from context.
|
||||
|
||||
Returns:
|
||||
str: Current trace ID or empty string
|
||||
"""
|
||||
return trace_id_var.get()
|
||||
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
Get logger instance.
|
||||
|
||||
Args:
|
||||
name: Logger name (usually __name__)
|
||||
|
||||
Returns:
|
||||
logging.Logger: Logger instance
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
class LoggerAdapter(logging.LoggerAdapter):
|
||||
"""
|
||||
Logger adapter that automatically includes trace context.
|
||||
"""
|
||||
|
||||
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
|
||||
"""
|
||||
Process log message to add trace context.
|
||||
|
||||
Args:
|
||||
msg: Log message
|
||||
kwargs: Keyword arguments
|
||||
|
||||
Returns:
|
||||
tuple: Processed message and kwargs
|
||||
"""
|
||||
trace_id = trace_id_var.get()
|
||||
if trace_id:
|
||||
extra = kwargs.get("extra", {})
|
||||
extra["trace_id"] = trace_id
|
||||
kwargs["extra"] = extra
|
||||
return msg, kwargs
|
||||
|
||||
|
||||
def get_logger_with_trace(name: str) -> LoggerAdapter:
|
||||
"""
|
||||
Get logger adapter with automatic trace context.
|
||||
|
||||
Args:
|
||||
name: Logger name (usually __name__)
|
||||
|
||||
Returns:
|
||||
LoggerAdapter: Logger adapter instance
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
return LoggerAdapter(logger, {})
|
||||
163
observability/tracing.py
Normal file
163
observability/tracing.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
OpenTelemetry tracing configuration with gRPC exporter.
|
||||
Provides distributed tracing for the application.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
|
||||
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
||||
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
||||
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
|
||||
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
|
||||
from core.config import settings
|
||||
|
||||
|
||||
tracer_provider: Optional[TracerProvider] = None
|
||||
span_processor: Optional[SpanProcessor] = None
|
||||
|
||||
|
||||
def init_tracing() -> trace.Tracer:
|
||||
"""
|
||||
Initialize OpenTelemetry tracing with gRPC exporter.
|
||||
|
||||
This should be called at application startup.
|
||||
|
||||
Returns:
|
||||
trace.Tracer: Configured tracer instance
|
||||
"""
|
||||
global tracer_provider, span_processor
|
||||
|
||||
if not settings.otel_enabled:
|
||||
# Return no-op tracer if OpenTelemetry is disabled
|
||||
return trace.get_tracer(__name__)
|
||||
|
||||
# Create resource with service name
|
||||
resource = Resource(attributes={
|
||||
SERVICE_NAME: settings.otel_service_name
|
||||
})
|
||||
|
||||
# Create sampler based on sample rate
|
||||
sampler = TraceIdRatioBased(settings.otel_sample_rate)
|
||||
|
||||
# Create tracer provider
|
||||
tracer_provider = TracerProvider(
|
||||
resource=resource,
|
||||
sampler=sampler
|
||||
)
|
||||
|
||||
# Create OTLP gRPC exporter
|
||||
otlp_exporter = OTLPSpanExporter(
|
||||
endpoint=settings.otel_exporter_endpoint,
|
||||
insecure=settings.otel_exporter_insecure
|
||||
)
|
||||
|
||||
# Create batch span processor
|
||||
span_processor = BatchSpanProcessor(otlp_exporter)
|
||||
tracer_provider.add_span_processor(span_processor)
|
||||
|
||||
# Set global tracer provider
|
||||
trace.set_tracer_provider(tracer_provider)
|
||||
|
||||
# Return tracer
|
||||
return trace.get_tracer(__name__)
|
||||
|
||||
|
||||
def instrument_app(app) -> None:
|
||||
"""
|
||||
Instrument FastAPI application with OpenTelemetry.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
"""
|
||||
if not settings.otel_enabled:
|
||||
return
|
||||
|
||||
# Instrument FastAPI
|
||||
FastAPIInstrumentor.instrument_app(app)
|
||||
|
||||
# Instrument HTTP client
|
||||
HTTPXClientInstrumentor().instrument()
|
||||
|
||||
# Redis instrumentation is done when Redis client is created
|
||||
# SQLAlchemy instrumentation is done when engine is created
|
||||
|
||||
|
||||
def instrument_sqlalchemy(engine) -> None:
|
||||
"""
|
||||
Instrument SQLAlchemy engine with OpenTelemetry.
|
||||
|
||||
Args:
|
||||
engine: SQLAlchemy engine instance
|
||||
"""
|
||||
if not settings.otel_enabled:
|
||||
return
|
||||
|
||||
SQLAlchemyInstrumentor().instrument(
|
||||
engine=engine.sync_engine
|
||||
)
|
||||
|
||||
|
||||
def instrument_redis() -> None:
|
||||
"""
|
||||
Instrument Redis client with OpenTelemetry.
|
||||
"""
|
||||
if not settings.otel_enabled:
|
||||
return
|
||||
|
||||
RedisInstrumentor().instrument()
|
||||
|
||||
|
||||
async def shutdown_tracing() -> None:
|
||||
"""
|
||||
Shutdown tracing and flush remaining spans.
|
||||
|
||||
This should be called at application shutdown.
|
||||
"""
|
||||
global tracer_provider, span_processor
|
||||
|
||||
if span_processor:
|
||||
span_processor.shutdown()
|
||||
|
||||
if tracer_provider:
|
||||
tracer_provider.shutdown()
|
||||
|
||||
|
||||
def get_tracer(name: str = __name__) -> trace.Tracer:
|
||||
"""
|
||||
Get tracer instance.
|
||||
|
||||
Args:
|
||||
name: Tracer name (usually __name__)
|
||||
|
||||
Returns:
|
||||
trace.Tracer: Tracer instance
|
||||
"""
|
||||
return trace.get_tracer(name)
|
||||
|
||||
|
||||
def get_current_span() -> trace.Span:
|
||||
"""
|
||||
Get current active span.
|
||||
|
||||
Returns:
|
||||
trace.Span: Current span
|
||||
"""
|
||||
return trace.get_current_span()
|
||||
|
||||
|
||||
def get_trace_id() -> str:
|
||||
"""
|
||||
Get current trace ID as hex string.
|
||||
|
||||
Returns:
|
||||
str: Trace ID or empty string if no active span
|
||||
"""
|
||||
span = get_current_span()
|
||||
if span and span.get_span_context().is_valid:
|
||||
return format(span.get_span_context().trace_id, '032x')
|
||||
return ""
|
||||
Reference in New Issue
Block a user