honcho/src/main.py

235 lines
6.7 KiB
Python

import logging
import re
import uuid
from collections.abc import Awaitable, Callable
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING
import sentry_sdk
from fastapi import FastAPI, Request, Response
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi_pagination import add_pagination
from pydantic import ValidationError
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.starlette import StarletteIntegration
from src import prometheus
from src.config import settings
from src.db import engine, request_context
from src.exceptions import HonchoException
from src.routers import (
keys,
messages,
peers,
sessions,
webhooks,
workspaces,
)
from src.security import create_admin_jwt
from src.sentry import initialize_sentry
from src.utils.logging import get_route_template
if TYPE_CHECKING:
from sentry_sdk._types import Event, Hint
def get_log_level() -> int:
"""
Convert log level string from settings to logging module constant.
Returns:
int: The logging level constant (e.g., logging.INFO)
"""
log_level_str = settings.LOG_LEVEL.upper()
log_levels = {
"CRITICAL": logging.CRITICAL, # 50
"ERROR": logging.ERROR, # 40
"WARNING": logging.WARNING, # 30
"INFO": logging.INFO, # 20
"DEBUG": logging.DEBUG, # 10
"NOTSET": logging.NOTSET, # 0
}
return log_levels.get(log_level_str, logging.INFO)
# Configure logging
logging.basicConfig(
level=get_log_level(),
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
# JWT Setup
async def setup_admin_jwt():
token = create_admin_jwt()
print(f"\n ADMIN JWT: {token}\n")
def before_send(event: "Event", hint: "Hint | None") -> "Event | None":
"""Filter out events raised from known non-actionable exceptions before Sentry sees them."""
if not hint:
return event
exc_info = hint.get("exc_info")
if not exc_info:
return event
_, exc_value, _ = exc_info
if isinstance(exc_value, HonchoException):
return None
# Filters out ValidationErrors and RequestValidationErrors (typically coming from Pydantic)
if isinstance(exc_value, ValidationError | RequestValidationError):
logger.info(f"Filtering out validation error from Sentry: {exc_value}")
return None
return event
# Sentry Setup
SENTRY_ENABLED = settings.SENTRY.ENABLED
if SENTRY_ENABLED:
initialize_sentry(
integrations=[
StarletteIntegration(
transaction_style="endpoint",
),
FastApiIntegration(
transaction_style="endpoint",
),
],
before_send=before_send,
)
@asynccontextmanager
async def lifespan(_: FastAPI):
yield
await engine.dispose()
app = FastAPI(
lifespan=lifespan,
servers=[
{"url": "http://localhost:8000", "description": "Local Development Server"},
{"url": "https://demo.honcho.dev", "description": "Demo Server"},
{"url": "https://api.honcho.dev", "description": "Production SaaS Platform"},
],
title="Honcho API",
summary="The Identity Layer for the Agentic World",
description="""Honcho is a platform for giving agents user-centric memory and social cognition""",
version="2.4.2",
contact={
"name": "Plastic Labs",
"url": "https://honcho.dev",
"email": "hello@plasticlabs.ai",
},
license_info={
"name": "GNU Affero General Public License v3.0",
"identifier": "AGPL-3.0-only",
"url": "https://github.com/plastic-labs/honcho/blob/main/LICENSE",
},
)
origins = [
"http://localhost",
"http://127.0.0.1:8000",
"https://demo.honcho.dev",
"https://api.honcho.dev",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
add_pagination(app)
app.include_router(workspaces.router, prefix="/v2")
app.include_router(peers.router, prefix="/v2")
app.include_router(sessions.router, prefix="/v2")
app.include_router(messages.router, prefix="/v2")
app.include_router(keys.router, prefix="/v2")
app.include_router(webhooks.router, prefix="/v2")
app.add_api_route("/metrics", prometheus.metrics, methods=["GET"])
# Global exception handlers
@app.exception_handler(HonchoException)
async def honcho_exception_handler(request: Request, exc: HonchoException):
"""Handle all Honcho-specific exceptions."""
logger.error(f"{exc.__class__.__name__}: {exc.detail}", exc_info=exc)
if prometheus.METRICS_ENABLED and request.url.path != "/metrics":
template = get_route_template(request)
prometheus.API_REQUESTS.labels(
method=request.method,
endpoint=template,
status_code=str(exc.status_code),
).inc()
return JSONResponse(
status_code=exc.status_code,
content={"detail": exc.detail},
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
"""Handle all unhandled exceptions."""
logger.error(f"Unhandled exception: {str(exc)}", exc_info=True)
if prometheus.METRICS_ENABLED and request.url.path != "/metrics":
template = get_route_template(request)
prometheus.API_REQUESTS.labels(
method=request.method,
endpoint=template,
status_code="500",
).inc()
if SENTRY_ENABLED:
sentry_sdk.capture_exception(exc)
return JSONResponse(
status_code=500,
content={"detail": "An unexpected error occurred"},
)
@app.middleware("http")
async def track_request(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
):
# Create a request ID that includes endpoint information
endpoint = re.sub(r"/[A-Za-z0-9_-]{21}", "", request.url.path).replace("/", "_")
request_id = f"{request.method}:{endpoint}:{str(uuid.uuid4())[:8]}"
# Store in request state and context var
request.state.request_id = request_id
token = request_context.set(f"api:{request_id}")
try:
response = await call_next(request)
# Track Prometheus metrics if enabled
if prometheus.METRICS_ENABLED and request.url.path != "/metrics":
template = get_route_template(request)
prometheus.API_REQUESTS.labels(
method=request.method,
endpoint=template,
status_code=str(response.status_code),
).inc()
return response
finally:
request_context.reset(token)