File size: 4,503 Bytes
db4ba8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd9584b
db4ba8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0ec2bd
 
 
 
 
 
db4ba8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
TradeFlow AI — FastAPI Application Entry Point

PRD §1.4 — Main application setup with lifespan management,
middleware, and router registration.
"""

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

try:
    import sentry_sdk
except Exception:  # pragma: no cover - optional dependency for observability
    sentry_sdk = None

import structlog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware

try:
    from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
except Exception:  # pragma: no cover - optional
    FastAPIInstrumentor = None

try:
    from prometheus_fastapi_instrumentator import Instrumentator
except Exception:  # pragma: no cover - optional
    Instrumentator = None

from .config import settings
from .dependencies import close_supabase, init_supabase
from .routers import admin, batches, blockchain, hs_recommend, vessel
from .utils.telemetry import setup_telemetry

log = structlog.get_logger()


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
    """Application lifespan — startup and shutdown."""
    log.info("TradeFlow AI starting up", environment=settings.ENVIRONMENT)

    # Initialize Sentry
    if settings.SENTRY_DSN:
        sentry_sdk.init(
            dsn=settings.SENTRY_DSN,
            traces_sample_rate=0.2,
            profiles_sample_rate=0.1,
            environment=settings.ENVIRONMENT,
        )

    # Initialize Supabase client
    await init_supabase()

    log.info("TradeFlow AI ready", version="1.0.0")
    yield

    # Shutdown
    await close_supabase()
    log.info("TradeFlow AI shutdown complete")


def create_app() -> FastAPI:
    app = FastAPI(
        title="TradeFlow AI API",
        description="Predictive Customs Intelligence Platform — Cikarang Dry Port",
        version="1.0.0",
        docs_url="/docs" if settings.ENVIRONMENT != "production" else None,
        redoc_url="/redoc" if settings.ENVIRONMENT != "production" else None,
        lifespan=lifespan,
    )

    # Setup OpenTelemetry & Prometheus (Phase 6)
    setup_telemetry(app)

    # ── Middleware ────────────────────────────────────────────────
    app.add_middleware(
        CORSMiddleware,
        allow_origins=settings.CORS_ORIGINS,
        allow_credentials=False,
        allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        allow_headers=["Content-Type", "Authorization"],
        expose_headers=["Content-Length"],
        max_age=3600,
    )
    app.add_middleware(GZipMiddleware, minimum_size=1000)

    # Rate limiting middleware (optional)
    try:
        from slowapi import Limiter
        from slowapi.util import get_remote_address
        limiter = Limiter(key_func=get_remote_address)
        app.state.limiter = limiter
    except Exception:
        app.state.limiter = None

    # ── Routers ───────────────────────────────────────────────────
    app.include_router(batches.router, prefix="/api/v1", tags=["batches"])
    app.include_router(hs_recommend.router, prefix="/api/v1", tags=["hs-recommend"])
    app.include_router(blockchain.router, prefix="/api/v1", tags=["blockchain"])
    app.include_router(vessel.router)
    app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])

    # ── Prometheus metrics (optional) ────────────────────────────
    # Disabled for HF Spaces deployment due to middleware crash on _IncludedRouter
    # if Instrumentator is not None:
    #     Instrumentator(
    #         should_group_status_codes=True,
    #         should_ignore_untemplated=True,
    #     ).instrument(app).expose(app, endpoint="/metrics")

    # ── OpenTelemetry (optional) ─────────────────────────────────
    if settings.OTEL_ENABLED and FastAPIInstrumentor is not None:
        FastAPIInstrumentor().instrument_app(app)

    return app


app = create_app()


@app.get("/health", tags=["health"])
async def health_check() -> dict[str, str]:
    """Health check endpoint — public, no auth required."""
    return {"status": "ok", "version": "1.0.0", "environment": settings.ENVIRONMENT}