Files

91 lines
3.1 KiB
Python
Raw Permalink Normal View History

import asyncio
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from opentelemetry import trace
2026-10-13 19:48:27 +03:30
from opentelemetry.trace import StatusCode
2026-10-13 08:45:53 +03:30
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from common.db import engine
from common.logging_config import configure_logging
2026-10-19 16:05:40 +03:30
from common.metrics import observe_db_pools
from common.telemetry import setup_telemetry
from . import chaos
from .repository import is_match
SERVICE_NAME = "sanctions"
logger = configure_logging(SERVICE_NAME)
tracer = trace.get_tracer(__name__)
setup_telemetry(service_name=SERVICE_NAME)
app = FastAPI(title=SERVICE_NAME)
2026-10-13 08:45:53 +03:30
FastAPIInstrumentor.instrument_app(
app,
excluded_urls="/healthz",
exclude_spans=["receive", "send"],
)
SQLAlchemyInstrumentor().instrument(engines=[engine.sync_engine])
2026-10-19 16:05:40 +03:30
observe_db_pools({"default": engine})
class LookupRequest(BaseModel):
holder_name: str
@app.get("/")
async def root():
return {"service": SERVICE_NAME, "version": "0.1.0"}
@app.get("/healthz")
async def healthz(request: Request):
if request.headers.get("X-Ledgerline-Probe") == "chatty-health":
logger.info("healthz_probe_received", probe="chatty-health")
return {"status": "ok", "service": SERVICE_NAME}
@app.get("/readyz")
async def readyz():
try:
async with engine.connect() as conn:
await conn.exec_driver_sql("SELECT 1")
except Exception as exc:
2026-10-13 19:48:27 +03:30
span = trace.get_current_span()
span.record_exception(exc)
span.set_status(StatusCode.ERROR, str(exc))
logger.exception("readyz_check_failed")
return JSONResponse(status_code=503, content={"status": "unavailable"})
return {"status": "ok"}
@app.post("/internal/lookup")
async def lookup(body: LookupRequest):
# holder_name is PII: logged by omission, not by redaction.
logger.info("lookup_received")
if await chaos.sanctions_errors_enabled() and chaos.should_error():
logger.info("chaos_engaged", chaos_flag="sanctions_errors")
return JSONResponse(status_code=503, content={"error": "sanctions vendor unavailable"})
if await chaos.slow_sanctions_enabled():
delay = chaos.sample_delay_seconds()
logger.info("chaos_engaged", chaos_flag="slow_sanctions", delay_ms=round(delay * 1000, 1))
await asyncio.sleep(delay)
# holder_name is PII: it stays off the span; only the outcome is recorded.
with tracer.start_as_current_span("sanctions.lookup_watchlist") as span:
match = await is_match(body.holder_name)
span.set_attribute("ledgerline.sanctions.matched", match is not None)
if match:
span.set_attribute("ledgerline.sanctions.list_source", match["list_source"])
logger.info(
"holder_screened",
matched=match is not None,
list_source=match["list_source"] if match else None,
)
if match:
return {"matched": True, **match}
return {"matched": False, "country": None, "list_source": None}