Spaces:
Build error
Build error
Upload folder using huggingface_hub
Browse files- app/core/usage_tracker.py +34 -0
- tests/test_usage_tracker.py +28 -0
app/core/usage_tracker.py
CHANGED
|
@@ -621,6 +621,40 @@ class UsageTracker:
|
|
| 621 |
self._mark_idempotent_key_used(idempotency_key)
|
| 622 |
return True, None
|
| 623 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
# --------------------------------------------------------------------------
|
| 625 |
# Legacy interface (kept for compatibility)
|
| 626 |
# --------------------------------------------------------------------------
|
|
|
|
| 621 |
self._mark_idempotent_key_used(idempotency_key)
|
| 622 |
return True, None
|
| 623 |
|
| 624 |
+
def _insert_audit_log(self, record: UsageRecord) -> None:
|
| 625 |
+
"""Insert a standalone usage_log row for a call whose quota was
|
| 626 |
+
already consumed at request time (see consume_quota_and_log) --
|
| 627 |
+
used by routes_governance.py's background tasks to record the
|
| 628 |
+
response body once it's known, under a distinct endpoint suffix
|
| 629 |
+
(e.g. ".../response"). Best-effort and logged, not raised, for the
|
| 630 |
+
same reason _record_pg_monthly_count is: this runs after the
|
| 631 |
+
response has already been sent to the caller, so it must not
|
| 632 |
+
surface as a request failure -- a background task exception here
|
| 633 |
+
is otherwise swallowed silently. `record.tier` is None at both real
|
| 634 |
+
call sites (tier only matters for quota consumption, already done
|
| 635 |
+
by the earlier consume_quota_and_log call for the same request),
|
| 636 |
+
but usage_log.tier is NOT NULL, so an absent tier is recorded as
|
| 637 |
+
"unknown" rather than raising or silently guessing a real tier."""
|
| 638 |
+
tier_value = record.tier.value if record.tier else "unknown"
|
| 639 |
+
try:
|
| 640 |
+
with self._get_conn() as conn:
|
| 641 |
+
conn.execute(
|
| 642 |
+
"""INSERT INTO usage_log
|
| 643 |
+
(api_key, tier, timestamp, endpoint, request_body, response, error,
|
| 644 |
+
processing_ms, idempotency_key)
|
| 645 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
| 646 |
+
(record.api_key, tier_value, record.timestamp, record.endpoint,
|
| 647 |
+
json.dumps(record.request_body) if record.request_body else None,
|
| 648 |
+
json.dumps(record.response) if record.response else None,
|
| 649 |
+
record.error, record.processing_ms, None)
|
| 650 |
+
)
|
| 651 |
+
conn.commit()
|
| 652 |
+
except Exception:
|
| 653 |
+
logger.error(
|
| 654 |
+
"Failed to insert audit log for api_key=%s endpoint=%s",
|
| 655 |
+
record.api_key, record.endpoint, exc_info=True,
|
| 656 |
+
)
|
| 657 |
+
|
| 658 |
# --------------------------------------------------------------------------
|
| 659 |
# Legacy interface (kept for compatibility)
|
| 660 |
# --------------------------------------------------------------------------
|
tests/test_usage_tracker.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
import psycopg2
|
|
@@ -149,3 +150,30 @@ def test_increment_usage_sync_succeeds_even_if_postgres_mirror_fails(tracker, mo
|
|
| 149 |
result = tracker.increment_usage_sync(record)
|
| 150 |
assert result is True
|
| 151 |
assert tracker.get_remaining_quota("mirror-fail-key", Tier.FREE) == 999
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
import os
|
| 3 |
|
| 4 |
import psycopg2
|
|
|
|
| 150 |
result = tracker.increment_usage_sync(record)
|
| 151 |
assert result is True
|
| 152 |
assert tracker.get_remaining_quota("mirror-fail-key", Tier.FREE) == 999
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def test_insert_audit_log_writes_response_row(tracker):
|
| 156 |
+
"""routes_governance.py schedules current_tracker._insert_audit_log as
|
| 157 |
+
a background task (background_tasks.add_task) to record the response
|
| 158 |
+
body once it's known, at app/api/routes_governance.py:407 and :738 --
|
| 159 |
+
always with tier=None, since quota was already consumed by an earlier
|
| 160 |
+
consume_quota_and_log call for the same request. The real UsageTracker
|
| 161 |
+
had no such method (only tests/conftest.py's MockTracker did), so every
|
| 162 |
+
real call raised AttributeError inside the background task (arf-api-002)."""
|
| 163 |
+
record = UsageRecord(
|
| 164 |
+
api_key="audit-log-key",
|
| 165 |
+
tier=None,
|
| 166 |
+
timestamp=time.time(),
|
| 167 |
+
endpoint="/api/v1/intents/evaluate/response",
|
| 168 |
+
request_body=None,
|
| 169 |
+
response={"recommended_action": "approve"},
|
| 170 |
+
processing_ms=12.5,
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
tracker._insert_audit_log(record)
|
| 174 |
+
|
| 175 |
+
logs = tracker.get_audit_logs("audit-log-key", limit=10)
|
| 176 |
+
assert len(logs) == 1
|
| 177 |
+
assert logs[0]["endpoint"] == "/api/v1/intents/evaluate/response"
|
| 178 |
+
assert logs[0]["tier"] == "unknown"
|
| 179 |
+
assert json.loads(logs[0]["response"]) == {"recommended_action": "approve"}
|