Restructure with Turborepo
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
class PIPException(Exception):
|
||||
def __init__(self, message: str, code: str = "PIP_ERROR", status_code: int = 500, details: dict | None = None):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.code = code
|
||||
self.status_code = status_code
|
||||
self.details = details or {}
|
||||
self.timestamp = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"error": {
|
||||
"code": self.code,
|
||||
"message": self.message,
|
||||
"details": self.details,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class NotFoundException(PIPException):
|
||||
def __init__(self, resource: str, resource_id: str | None = None):
|
||||
msg = f"{resource} not found"
|
||||
if resource_id:
|
||||
msg = f"{resource} '{resource_id}' not found"
|
||||
super().__init__(message=msg, code="NOT_FOUND", status_code=404, details={"resource": resource, "resource_id": resource_id})
|
||||
|
||||
|
||||
class ConflictException(PIPException):
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
super().__init__(message=message, code="CONFLICT", status_code=409, details=details)
|
||||
|
||||
|
||||
class ValidationException(PIPException):
|
||||
def __init__(self, message: str, details: dict | None = None):
|
||||
super().__init__(message=message, code="VALIDATION_ERROR", status_code=422, details=details)
|
||||
|
||||
|
||||
class UnauthorizedException(PIPException):
|
||||
def __init__(self, message: str = "Authentication required"):
|
||||
super().__init__(message=message, code="UNAUTHORIZED", status_code=401)
|
||||
|
||||
|
||||
class ForbiddenException(PIPException):
|
||||
def __init__(self, message: str = "Insufficient permissions", required_role: str | None = None):
|
||||
details = {}
|
||||
if required_role:
|
||||
details["required_role"] = required_role
|
||||
super().__init__(message=message, code="FORBIDDEN", status_code=403, details=details)
|
||||
|
||||
|
||||
class RateLimitException(PIPException):
|
||||
def __init__(self, message: str = "Rate limit exceeded"):
|
||||
super().__init__(message=message, code="RATE_LIMIT_EXCEEDED", status_code=429)
|
||||
|
||||
|
||||
class ServiceUnavailableException(PIPException):
|
||||
def __init__(self, service: str, message: str | None = None):
|
||||
msg = message or f"Service '{service}' is unavailable"
|
||||
super().__init__(message=msg, code="SERVICE_UNAVAILABLE", status_code=503, details={"service": service})
|
||||
|
||||
|
||||
class ConnectorException(PIPException):
|
||||
def __init__(self, connector_type: str, message: str, details: dict | None = None):
|
||||
d = details or {}
|
||||
d["connector_type"] = connector_type
|
||||
super().__init__(message=message, code="CONNECTOR_ERROR", status_code=502, details=d)
|
||||
|
||||
|
||||
class SyncException(PIPException):
|
||||
def __init__(self, pharmacy_id: str, message: str, details: dict | None = None):
|
||||
d = details or {}
|
||||
d["pharmacy_id"] = pharmacy_id
|
||||
super().__init__(message=message, code="SYNC_ERROR", status_code=502, details=d)
|
||||
@@ -0,0 +1,56 @@
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
|
||||
from src.infrastructure.config.settings import settings
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
shared_processors: list[Any] = [
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.UnicodeDecoder(),
|
||||
]
|
||||
|
||||
if settings.LOG_JSON_FORMAT:
|
||||
renderer = structlog.processors.JSONRenderer()
|
||||
else:
|
||||
renderer = structlog.dev.ConsoleRenderer()
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
*shared_processors,
|
||||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||||
],
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
formatter = structlog.stdlib.ProcessorFormatter(
|
||||
processors=[
|
||||
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
||||
renderer,
|
||||
],
|
||||
foreign_pre_chain=shared_processors,
|
||||
)
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.handlers.clear()
|
||||
root_logger.addHandler(handler)
|
||||
root_logger.setLevel(settings.LOG_LEVEL.upper())
|
||||
|
||||
for noisy in ("uvicorn.access", "sqlalchemy.engine", "httpx", "aioredis"):
|
||||
logging.getLogger(noisy).setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def get_logger(name: str | None = None) -> Any:
|
||||
return structlog.get_logger(name)
|
||||
@@ -0,0 +1,94 @@
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Request, Response
|
||||
from prometheus_client import Counter, Gauge, Histogram
|
||||
|
||||
REQUEST_COUNT = Counter(
|
||||
"pip_http_requests_total",
|
||||
"Total HTTP requests",
|
||||
["method", "endpoint", "status_code"],
|
||||
)
|
||||
|
||||
REQUEST_DURATION = Histogram(
|
||||
"pip_http_request_duration_seconds",
|
||||
"HTTP request duration in seconds",
|
||||
["method", "endpoint"],
|
||||
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0],
|
||||
)
|
||||
|
||||
REQUESTS_IN_PROGRESS = Gauge(
|
||||
"pip_http_requests_in_progress",
|
||||
"HTTP requests currently in progress",
|
||||
["method", "endpoint"],
|
||||
)
|
||||
|
||||
DB_QUERY_DURATION = Histogram(
|
||||
"pip_db_query_duration_seconds",
|
||||
"Database query duration in seconds",
|
||||
["operation"],
|
||||
buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0],
|
||||
)
|
||||
|
||||
CONNECTOR_OPERATIONS = Counter(
|
||||
"pip_connector_operations_total",
|
||||
"Total connector operations",
|
||||
["connector_type", "operation", "status"],
|
||||
)
|
||||
|
||||
SYNC_JOBS = Counter(
|
||||
"pip_sync_jobs_total",
|
||||
"Total sync jobs",
|
||||
["strategy", "status"],
|
||||
)
|
||||
|
||||
SYNC_DURATION = Histogram(
|
||||
"pip_sync_duration_seconds",
|
||||
"Sync job duration in seconds",
|
||||
["strategy"],
|
||||
buckets=[1.0, 5.0, 10.0, 30.0, 60.0, 300.0, 600.0, 1800.0],
|
||||
)
|
||||
|
||||
ACTIVE_CONNECTIONS = Gauge(
|
||||
"pip_active_connections",
|
||||
"Active infrastructure connections",
|
||||
["type"],
|
||||
)
|
||||
|
||||
CACHE_OPERATIONS = Counter(
|
||||
"pip_cache_operations_total",
|
||||
"Cache operations",
|
||||
["operation", "result"],
|
||||
)
|
||||
|
||||
RESERVATION_OPERATIONS = Counter(
|
||||
"pip_reservation_operations_total",
|
||||
"Reservation operations",
|
||||
["operation", "status"],
|
||||
)
|
||||
|
||||
|
||||
async def metrics_middleware(request: Request, call_next: Callable) -> Response:
|
||||
if request.url.path == "/metrics" or request.url.path.startswith("/docs") or request.url.path.startswith("/openapi"):
|
||||
return await call_next(request)
|
||||
|
||||
method = request.method
|
||||
endpoint = request.url.path
|
||||
|
||||
REQUESTS_IN_PROGRESS.labels(method=method, endpoint=endpoint).inc()
|
||||
|
||||
start_time = time.perf_counter()
|
||||
try:
|
||||
response = await call_next(request)
|
||||
status_code = response.status_code
|
||||
except Exception:
|
||||
status_code = 500
|
||||
raise
|
||||
finally:
|
||||
duration = time.perf_counter() - start_time
|
||||
REQUEST_COUNT.labels(method=method, endpoint=endpoint, status_code=status_code).inc()
|
||||
REQUEST_DURATION.labels(method=method, endpoint=endpoint).observe(duration)
|
||||
REQUESTS_IN_PROGRESS.labels(method=method, endpoint=endpoint).dec()
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
|
||||
from opentelemetry.instrumentation.redis import RedisInstrumentor
|
||||
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
|
||||
from src.infrastructure.config.settings import settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine
|
||||
|
||||
|
||||
def setup_tracing(app: Any = None, engine: AsyncEngine | None = None) -> None:
|
||||
if not settings.OTEL_TRACES_ENABLED:
|
||||
return
|
||||
|
||||
resource = Resource.create({"service.name": settings.OTEL_SERVICE_NAME})
|
||||
|
||||
provider = TracerProvider(resource=resource)
|
||||
|
||||
otlp_exporter = OTLPSpanExporter(endpoint=settings.OTEL_EXPORTER_OTLP_ENDPOINT)
|
||||
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
|
||||
|
||||
trace.set_tracer_provider(provider)
|
||||
|
||||
if engine is not None:
|
||||
sync_engine = engine.sync_engine if hasattr(engine, "sync_engine") else engine
|
||||
SQLAlchemyInstrumentor().instrument(engine=sync_engine)
|
||||
RedisInstrumentor().instrument()
|
||||
|
||||
if app is not None:
|
||||
FastAPIInstrumentor.instrument_app(app)
|
||||
|
||||
|
||||
def get_tracer(name: str = "pip-platform") -> trace.Tracer:
|
||||
return trace.get_tracer(name)
|
||||
Reference in New Issue
Block a user