Add business spans and silence the frontend health poll

Manual INTERNAL spans for the business steps the auto-instrumentation
can't name: fraud.score_transaction, sanctions.lookup_watchlist,
ledger.settle and insights.aggregate_120d, with ledgerline.* attributes
(account, amount, risk score, match outcome). holder_name stays off the
spans. Each span starts after its handler's chaos flag reads and
injection points, and no chaos-derived values become attributes.

The gateway's /api/health/all fan-out now runs under
suppress_instrumentation: with the route excluded, its five downstream
/healthz calls were each a parentless root trace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Movses Aslanyan
2026-10-13 18:11:38 +03:30
co-authored by Claude Opus 5.5
parent d1c158c835
commit d52a19965a
5 changed files with 88 additions and 48 deletions
+31 -18
View File
@@ -2,6 +2,7 @@ import httpx
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from common.http import downstream_client
@@ -15,6 +16,7 @@ from .scoring import score
SERVICE_NAME = "fraud"
logger = configure_logging(SERVICE_NAME)
tracer = trace.get_tracer(__name__)
setup_telemetry(service_name=SERVICE_NAME)
app = FastAPI(title=SERVICE_NAME)
@@ -64,23 +66,34 @@ async def score_transaction(body: ScoreRequest):
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}")
with tracer.start_as_current_span(
"fraud.score_transaction",
attributes={
"ledgerline.account.id": body.account_id,
"ledgerline.tx.amount_minor": body.amount_minor,
},
) as span:
if body.category_id is not None:
span.set_attribute("ledgerline.merchant.category_id", body.category_id)
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"})
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}
matched = resp.json().get("matched", False)
risk_score = score(body.category_id, matched)
span.set_attribute("ledgerline.sanctions.matched", matched)
span.set_attribute("ledgerline.risk.score", risk_score)
return {"risk_score": risk_score, "matched": matched}