first (big) draft

This commit is contained in:
2026-08-07 17:19:49 +02:00
parent 2974a3a4cd
commit 9e78dfa4f2
24 changed files with 3975 additions and 1 deletions
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
collect.py — Point d'entrée du cron. Collecte le coût de la veille (J-1)
pour chaque tenant défini dans config.json, et l'écrit dans SQLite.
Pourquoi J-1 et pas "aujourd'hui" : les données de facturation AWS/Azure du
jour même sont généralement incomplètes (délai de consolidation de
quelques heures à ~24-48h côté AWS Cost Explorer notamment). Collecter J-1
donne un chiffre stable.
Utilisation (cron quotidien, par exemple à 6h du matin) :
python collect.py --config /path/to/config.json --db /path/to/cost_dashboard.db
Un échec sur un tenant n'empêche pas la collecte des autres : chaque
tenant est traité indépendamment et les erreurs sont journalisées en base
(status='error') pour que le dashboard puisse l'afficher plutôt que de
silencieusement afficher un trou ou un zéro.
"""
import argparse
import json
import logging
import sys
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from db import (
get_connection, init_db, upsert_collection,
upsert_resource_costs, set_resource_collection_status,
)
from providers import (
get_aws_cost, get_azure_cost, convert_result,
get_aws_resource_costs, get_azure_resource_costs, convert_resource_costs,
ResourceLevelUnavailable,
)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
log = logging.getLogger("collect")
TARGET_CURRENCY = "EUR"
# Fenêtre du détail par ressource (bucket S3, instance EC2...) : bornée à 14
# jours côté AWS Cost Explorer (limite dure de l'API) ; gardée identique côté
# Azure pour rester cohérent entre providers. Voir providers.py.
RESOURCE_WINDOW_DAYS = 14
def collect_tenant(conn, provider: str, entry: dict, period_start: str, period_end: str) -> bool:
"""Collecte le relevé quotidien (par service) d'un tenant. Retourne True
si la collecte a réussi — sert à décider si ça vaut la peine de tenter
le détail par ressource juste après (inutile si l'auth du tenant est
déjà cassée)."""
name = entry["name"]
collected_at = datetime.now(timezone.utc).isoformat()
try:
if provider == "aws":
data = get_aws_cost(entry, period_start, period_end, granularity="DAILY")
else:
data = get_azure_cost(
entry["tenant_id"], entry["client_id"], entry["client_secret"],
entry["subscription_id"], period_start, period_end,
)
data = convert_result(data, TARGET_CURRENCY)
upsert_collection(
conn, provider, name, "daily", period_start, period_end,
collected_at, "ok", data=data,
)
log.info("OK %-6s %-30s %.2f %s", provider, name, data["total"], data["currency"])
return True
except Exception as e:
upsert_collection(
conn, provider, name, "daily", period_start, period_end,
collected_at, "error", error_message=str(e),
)
log.error("ECHEC %-6s %-30s %s", provider, name, e)
return False
def collect_tenant_resources(conn, provider: str, entry: dict, period_start: str, period_end: str) -> None:
"""Collecte le détail par ressource (RESOURCE_WINDOW_DAYS derniers
jours) d'un tenant, pour le panneau de détail du dashboard. Séparé de
collect_tenant : ça peut échouer pour une raison indépendante (ex:
"Resource IDs" non activé côté AWS) sans que la collecte quotidienne
normale soit affectée."""
name = entry["name"]
collected_at = datetime.now(timezone.utc).isoformat()
try:
if provider == "aws":
data = get_aws_resource_costs(entry, period_start, period_end)
else:
data = get_azure_resource_costs(
entry["tenant_id"], entry["client_id"], entry["client_secret"],
entry["subscription_id"], period_start, period_end,
)
data = convert_resource_costs(data, TARGET_CURRENCY)
upsert_resource_costs(conn, provider, name, period_start, period_end, data["services"])
set_resource_collection_status(conn, provider, name, "ok", collected_at)
n_resources = sum(len(v) for v in data["services"].values())
log.info("OK %-6s %-30s détail ressources (%d ressources)", provider, name, n_resources)
except ResourceLevelUnavailable as e:
set_resource_collection_status(conn, provider, name, "unavailable", collected_at, message=str(e))
log.warning("INDISPO %-6s %-30s détail ressources indisponible : %s", provider, name, e)
except Exception as e:
set_resource_collection_status(conn, provider, name, "error", collected_at, message=str(e))
log.error("ECHEC %-6s %-30s détail ressources : %s", provider, name, e)
def main():
parser = argparse.ArgumentParser(description="Collecte journalière des coûts (cron)")
parser.add_argument("--config", default="config.json")
parser.add_argument("--db", default=None, help="Chemin vers la base SQLite")
parser.add_argument("--date", help="Jour à collecter (YYYY-MM-DD), défaut: hier")
args = parser.parse_args()
if args.date:
target_day = date.fromisoformat(args.date)
else:
target_day = date.today() - timedelta(days=1)
period_start = target_day.isoformat()
period_end = (target_day + timedelta(days=1)).isoformat()
db_path = args.db or None
if db_path:
init_db(db_path)
conn = get_connection(db_path)
else:
init_db()
conn = get_connection()
with open(args.config) as f:
config = json.load(f)
log.info("Collecte pour le %s", period_start)
resource_period_end = target_day + timedelta(days=1)
resource_period_start = resource_period_end - timedelta(days=RESOURCE_WINDOW_DAYS)
for provider, entries in (("aws", config.get("aws", [])), ("azure", config.get("azure", []))):
for entry in entries:
ok = collect_tenant(conn, provider, entry, period_start, period_end)
if ok:
collect_tenant_resources(
conn, provider, entry,
resource_period_start.isoformat(), resource_period_end.isoformat(),
)
conn.close()
log.info("Terminé.")
if __name__ == "__main__":
main()