Implement client cost attribution

This commit is contained in:
tegwick 2026-08-10 20:32:09 +02:00
parent d2b9bc4b32
commit 33883e0977
15 changed files with 362 additions and 21 deletions

View file

@ -6,6 +6,8 @@ from collections import defaultdict
from dataclasses import dataclass
from typing import Iterable
from fin_hub.attribution import optional_attribution
@dataclass(frozen=True)
class ServiceCostLine:
@ -15,6 +17,19 @@ class ServiceCostLine:
amount: float
currency: str
source: str
client_id: str | None = None
application_id: str | None = None
app_instance_id: str | None = None
cost_attribution_key: str | None = None
def __post_init__(self) -> None:
attribution = optional_attribution(
self.client_id, self.application_id, self.app_instance_id
)
expected_key = attribution.key if attribution else None
if self.cost_attribution_key not in (None, expected_key):
raise ValueError("cost_attribution_key does not match its attribution dimensions")
object.__setattr__(self, "cost_attribution_key", expected_key)
def build_service_cost_report(
@ -22,6 +37,7 @@ def build_service_cost_report(
) -> dict:
by_service: dict[str, dict] = {}
totals_by_month: dict[str, float] = defaultdict(float)
by_attribution: dict[tuple[str, str], dict] = {}
for line in lines:
bucket = by_service.setdefault(
line.service_id,
@ -37,11 +53,31 @@ def build_service_cost_report(
bucket["months"][line.period_month] = month_total
bucket["total"] += line.amount
totals_by_month[line.period_month] += line.amount
key = (line.cost_attribution_key or "unattributed", line.currency)
attribution = by_attribution.setdefault(
key,
{
"cost_attribution_key": line.cost_attribution_key,
"client_id": line.client_id,
"application_id": line.application_id,
"app_instance_id": line.app_instance_id,
"currency": line.currency,
"months": {},
"total": 0.0,
},
)
attribution["months"][line.period_month] = (
attribution["months"].get(line.period_month, 0.0) + line.amount
)
attribution["total"] += line.amount
services = sorted(by_service.values(), key=lambda item: item["total"], reverse=True)
return {
"source_hub": "fin-hub",
"target_hub": "ops-hub",
"signal": "service_cost_attribution",
"services": services,
"attributions": sorted(
by_attribution.values(), key=lambda item: item["total"], reverse=True
),
"totals_by_month": dict(sorted(totals_by_month.items())),
}
}