mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
Hanzo CI/CD / cicd (push) Successful in 8m30s
CI/CD / cicd (push) Successful in 8m30s
The cache client is now the KV client (core/cache/kv.py, kv_client), configured by KV_URL. rate_limit_check allows the request and logs when KV is unreachable, and the RPC cache treats an outage as a miss. cloud_billing reads KV directly instead of a missing usage_tracker.redis attribute; the NFT refresh enqueues through an arq pool. Co-authored-by: Hanzo Dev <[email protected]>
167 lines
5.2 KiB
Python
167 lines
5.2 KiB
Python
"""Bootnode API - Main application entry point."""
|
|
|
|
from collections.abc import AsyncGenerator
|
|
from contextlib import asynccontextmanager
|
|
|
|
import structlog
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import ORJSONResponse
|
|
|
|
from bootnode import __version__
|
|
from bootnode.api import router as api_router
|
|
from bootnode.config import get_settings
|
|
from bootnode.core.cache import kv_client
|
|
from bootnode.core.datastore import datastore_client
|
|
from bootnode.core.kms import inject_secrets
|
|
from bootnode.db.session import engine, init_db
|
|
from bootnode.ws import router as ws_router
|
|
from bootnode.zap import start_zap_server, stop_zap_server
|
|
|
|
# Load secrets from Hanzo KMS before settings
|
|
inject_secrets()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Hanzo O11y — Traces + Metrics + Logs
|
|
# All telemetry → OTEL Collector → Datastore + Loki
|
|
# Prometheus /metrics served natively for ServiceMonitor scraping
|
|
# ---------------------------------------------------------------------------
|
|
from bootnode.core.o11y import setup_otel, setup_otel_logging
|
|
|
|
# Wire OTEL log export early (before any logging)
|
|
setup_otel_logging()
|
|
|
|
logger = structlog.get_logger()
|
|
settings = get_settings()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
|
"""Application lifespan handler."""
|
|
# Startup
|
|
logger.info("Starting Bootnode API", env=settings.app_env)
|
|
await init_db()
|
|
await kv_client.initialize()
|
|
|
|
# Initialize DataStore - optional, won't fail startup
|
|
try:
|
|
await datastore_client.initialize()
|
|
logger.info("DataStore connected")
|
|
except Exception as e:
|
|
logger.warning("DataStore not available", error=str(e))
|
|
|
|
# Start native ZAP server (Cap'n Proto RPC over TCP)
|
|
zap_server = None
|
|
if settings.zap_enabled:
|
|
try:
|
|
zap_server = await start_zap_server(
|
|
host=settings.zap_host,
|
|
port=settings.zap_port,
|
|
)
|
|
logger.info(
|
|
"ZAP server started",
|
|
url=f"zap://{settings.zap_host}:{settings.zap_port}",
|
|
)
|
|
except Exception as e:
|
|
logger.warning("ZAP server failed to start", error=str(e))
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
logger.info("Shutting down Bootnode API")
|
|
|
|
# Stop ZAP server
|
|
if zap_server:
|
|
try:
|
|
await stop_zap_server()
|
|
logger.info("ZAP server stopped")
|
|
except Exception as e:
|
|
logger.warning("ZAP server stop error", error=str(e))
|
|
|
|
await engine.dispose()
|
|
await kv_client.close()
|
|
await datastore_client.close()
|
|
|
|
|
|
app = FastAPI(
|
|
title="Bootnode API",
|
|
description="Blockchain Development Platform - Multi-chain RPC, Token API, NFT API, Smart Wallets, and more",
|
|
version=__version__,
|
|
docs_url="/docs" if not settings.is_production else None,
|
|
redoc_url="/redoc" if not settings.is_production else None,
|
|
openapi_url="/openapi.json" if not settings.is_production else None,
|
|
default_response_class=ORJSONResponse,
|
|
lifespan=lifespan,
|
|
)
|
|
|
|
# Hanzo O11y — /metrics + OTEL traces/metrics/logs
|
|
setup_otel(app)
|
|
|
|
# Validate production secrets (fail-fast)
|
|
settings.validate_production()
|
|
|
|
# CORS - all cloud domains for white-label multi-network support
|
|
production_origins = [
|
|
"https://bootno.de",
|
|
"https://lux.build",
|
|
"https://cloud.lux.network",
|
|
"https://lux.cloud",
|
|
"https://console.lux.cloud",
|
|
"https://cloud.pars.network",
|
|
"https://cloud.zoo.network",
|
|
"https://cloud.hanzo.ai",
|
|
"https://docs.bootno.de",
|
|
]
|
|
# Add frontend_url if set
|
|
if settings.frontend_url:
|
|
origin = settings.frontend_url.rstrip("/")
|
|
if origin not in production_origins:
|
|
production_origins.append(origin)
|
|
# Add extra origins from ALLOWED_ORIGINS env var
|
|
for extra in getattr(settings, "allowed_origins", []):
|
|
origin = extra.strip().rstrip("/")
|
|
if origin and origin not in production_origins:
|
|
production_origins.append(origin)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"] if not settings.is_production else production_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Include API routes
|
|
app.include_router(api_router, prefix=settings.api_prefix)
|
|
|
|
# Include WebSocket routes
|
|
app.include_router(ws_router, prefix=f"{settings.api_prefix}/ws", tags=["WebSocket"])
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check() -> dict:
|
|
"""Health check endpoint."""
|
|
return {"status": "healthy", "version": __version__}
|
|
|
|
|
|
@app.get("/")
|
|
async def root() -> dict:
|
|
"""Root endpoint.
|
|
|
|
`docs` mirrors app.docs_url rather than hardcoding "/docs". The FastAPI app
|
|
above disables docs/redoc/openapi in production on purpose, so advertising
|
|
"/docs" unconditionally pointed every caller of the public API root at a URL
|
|
that 404s — api.bootno.de says `"docs": "/docs"` and /docs returns
|
|
{"detail":"Not Found"}. Reading the real value keeps the two in step however
|
|
the setting is flipped, instead of leaving a second place to remember.
|
|
"""
|
|
body = {
|
|
"name": "Bootnode API",
|
|
"version": __version__,
|
|
"status": "operational",
|
|
}
|
|
if app.docs_url:
|
|
body["docs"] = app.docs_url
|
|
return body
|