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:
danial
2025-11-01 14:32:29 +08:00
commit 0e41e7acce
46 changed files with 7025 additions and 0 deletions

View File

187
observability/logging.py Normal file
View 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
View 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 ""