Files
LedgerLine/services/fraud/app.py
T
Movses AslanyanandClaude Opus 5.5 14938ab36b Propagate traces over HTTP and add Redis spans
setup_telemetry now instruments httpx and redis process-wide. Both patch
the client classes, so clients created at import time (gateway's
_proxy_client, common.redis_client) are covered too. httpx CLIENT spans
inject traceparent, so the downstream FastAPI SERVER spans join the
caller's trace. gateway and fraud get setup_telemetry, the FastAPI span,
and the OTLP auth header. All seven services now export traces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:28:03 +03:30

87 lines
2.9 KiB
Python

import httpx
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from common.http import downstream_client
from common.logging_config import configure_logging
from common.redis_client import redis_client
from common.settings import settings
from common.telemetry import setup_telemetry
from . import chaos
from .scoring import score
SERVICE_NAME = "fraud"
logger = configure_logging(SERVICE_NAME)
setup_telemetry(service_name=SERVICE_NAME)
app = FastAPI(title=SERVICE_NAME)
FastAPIInstrumentor.instrument_app(
app,
excluded_urls="/healthz",
exclude_spans=["receive", "send"],
)
class ScoreRequest(BaseModel):
account_id: str
holder_name: str
category_id: int | None = None
amount_minor: int
request_id: str = "unknown"
@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("received chatty-health healthz probe from gateway")
return {"status": "ok", "service": SERVICE_NAME}
@app.get("/readyz")
async def readyz():
try:
await redis_client.ping()
except Exception as exc:
logger.exception(f"readyz check failed: {exc}")
return JSONResponse(status_code=503, content={"status": "unavailable"})
return {"status": "ok"}
@app.post("/internal/score")
async def score_transaction(body: ScoreRequest):
logger.info(f"scoring account {body.account_id} request_id={body.request_id}")
await redis_client.incr("fraud:calls_total")
if await chaos.blocking_event_loop_enabled():
logger.info(f"blocking_event_loop chaos engaged for request_id={body.request_id}")
chaos.synchronous_cpu_work()
async with downstream_client(body.request_id) as client:
try:
resp = await client.post(
f"{settings.sanctions_url}/internal/lookup",
json={"holder_name": body.holder_name, "request_id": body.request_id},
)
except httpx.TimeoutException:
logger.exception(f"sanctions lookup timed out for request_id={body.request_id}")
return JSONResponse(status_code=504, content={"error": "sanctions lookup timed out"})
except httpx.RequestError as exc:
logger.exception(f"sanctions lookup failed for request_id={body.request_id}: {exc}")
return JSONResponse(status_code=502, content={"error": "sanctions lookup failed"})
if resp.status_code >= 300:
logger.info(f"sanctions returned {resp.status_code} for request_id={body.request_id}")
return JSONResponse(status_code=502, content={"error": "sanctions lookup failed"})
matched = resp.json().get("matched", False)
risk_score = score(body.category_id, matched)
return {"risk_score": risk_score, "matched": matched}