first (big) draft
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user