Files
cloudinit-bot 9228abdb21 feat(scripts): cost_report.py — monthly NocoDB/S3/Ollama rollup (REQ-INB-54)
---ci---
project: inboxos
phase: 1
milestone: v1.4
status: execute
wave: 1
task: T6
---/ci---
2026-06-09 14:07:41 +00:00

122 lines
3.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Cost report: monthly NocoDB / S3 / Ollama rollup.
v1.4 REQ-INB-54.
"""
from __future__ import annotations
import argparse
import calendar
import json
import os
import sys
from datetime import datetime, timezone
import requests
NOCODB_BASE_URL = os.environ.get("NOCODB_BASE_URL", "http://localhost:8080")
NOCODB_API_TOKEN = os.environ.get("NOCODB_API_TOKEN", "")
SEAWEEDFS_S3_BUCKET = os.environ.get("SEAWEEDFS_S3_BUCKET", "inboxos")
def _nocodb_headers() -> dict[str, str]:
return {"xc-token": NOCODB_API_TOKEN, "Content-Type": "application/json"}
def nocodb_row_counts() -> dict[str, int]:
if not NOCODB_API_TOKEN:
return {"status": "unreachable", "reason": "NOCODB_API_TOKEN unset"}
counts: dict[str, int] = {"status": "ok"}
for module in ("finance", "crm", "intel", "promotions"):
table_id = os.environ.get(f"NOCODB_TABLE_{module.upper()}", "")
if not table_id:
counts[module] = 0
continue
try:
r = requests.get(
f"{NOCODB_BASE_URL}/api/v2/tables/{table_id}/records",
headers=_nocodb_headers(),
params={"limit": 1, "where": "(Id,gt,0)"},
timeout=10,
)
r.raise_for_status()
counts[module] = r.json().get("pageInfo", {}).get("totalRows", 0)
except requests.RequestException as exc:
counts["status"] = "unreachable"
counts[f"{module}_error"] = str(exc)
counts[module] = 0
return counts
def s3_archive_size_gb() -> float:
try:
import boto3
s3 = boto3.client("s3")
resp = s3.list_objects_v2(Bucket=SEAWEEDFS_S3_BUCKET)
total = sum(obj["Size"] for obj in resp.get("Contents", []))
return round(total / (1024 ** 3), 3)
except Exception as exc:
return -1.0
def ollama_token_count(month: str) -> int:
metrics_path = os.path.join("pipelines", "_metrics", f"tokens-{month}.jsonl")
if not os.path.exists(metrics_path):
return 0
total = 0
with open(metrics_path) as f:
for line in f:
try:
total += json.loads(line).get("total_tokens", 0)
except json.JSONDecodeError:
continue
return total
def report(month: str) -> list[dict]:
year, mo = map(int, month.split("-"))
_, days_in_month = calendar.monthrange(year, mo)
nocodb = nocodb_row_counts()
s3_gb = s3_archive_size_gb()
tokens = ollama_token_count(month)
rows = []
for day in range(1, days_in_month + 1):
rows.append({
"date": f"{month}-{day:02d}",
"nocodb": nocodb,
"s3_archive_gb": s3_gb,
"ollama_tokens": tokens,
})
return rows
def main() -> int:
p = argparse.ArgumentParser(description="Monthly NocoDB/S3/Ollama cost report")
p.add_argument("--month", help="YYYY-MM (default: current month)")
p.add_argument("--format", choices=["json", "csv"], default="json")
args = p.parse_args()
if not args.month:
args.month = datetime.now(timezone.utc).strftime("%Y-%m")
rows = report(args.month)
if args.format == "json":
print(json.dumps(rows, indent=2))
else:
print("date,nocodb_status,finance,crm,intel,promotions,s3_archive_gb,ollama_tokens")
for r in rows:
n = r["nocodb"]
print(
f"{r['date']},{n.get('status', '?')},"
f"{n.get('finance', 0)},{n.get('crm', 0)},"
f"{n.get('intel', 0)},{n.get('promotions', 0)},"
f"{r['s3_archive_gb']},{r['ollama_tokens']}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())