Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 6,532 Bytes
13170f7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | """Lightweight usage event logger for HF Space demos.
Appends JSONL events to a local folder, which huggingface_hub's CommitScheduler
periodically pushes to a private HF Dataset repo. Auto-enables on HF Spaces;
disabled locally unless USAGE_ANALYTICS=1.
"""
from __future__ import annotations
import io
import json
import os
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import TYPE_CHECKING
from log_utils import setup_logger
if TYPE_CHECKING:
from huggingface_hub import CommitInfo
logger = setup_logger("UsageTracker")
def _make_cleanup_scheduler(active_log_file: Path, **kwargs: object) -> object:
"""Build a CommitScheduler that deletes pushed files (except the active one).
Prevents stale files from being re-uploaded under a different ``path_in_repo``
if the demo name changes between process restarts.
"""
from huggingface_hub import CommitScheduler
class _Cleanup(CommitScheduler):
def __init__(self, active_file: Path, **kw: object) -> None:
self._active_file = active_file.resolve()
super().__init__(**kw)
def push_to_hub(self) -> CommitInfo | None:
result = super().push_to_hub()
if result is None:
return None
with self.lock:
for path in sorted(self.folder_path.glob("**/*")):
if path.is_file() and path.resolve() != self._active_file:
path.unlink(missing_ok=True)
self.last_uploaded.pop(path, None)
return result
return _Cleanup(active_file=active_log_file, **kwargs)
class UsageTracker:
"""Append-only usage event logger backed by a private HF Dataset repo.
Events are written as JSONL to a local folder. A background thread
(CommitScheduler) batches and pushes them to the dataset repo every
``push_interval_minutes``. Thread-safe, never raises from ``log()``.
Args:
repo_id: Target HF dataset repo
(e.g. ``"LatticeSemi/STAGING-Demo-Analytics-v1.0"``).
push_interval_minutes: How often CommitScheduler pushes to HF Hub.
enabled: Explicit on/off. ``None`` = auto-detect (on when ``SPACE_ID``
env var is set, off otherwise). The ``USAGE_ANALYTICS`` env var
always takes precedence over both this parameter and ``SPACE_ID``.
"""
def __init__(
self,
repo_id: str,
push_interval_minutes: int = 10,
enabled: bool | None = None,
) -> None:
self._enabled = False
self._log_file: Path | None = None
self._fh: io.TextIOWrapper | None = None
self._scheduler: object | None = None
should_enable = self._resolve_enabled(enabled)
if not should_enable:
logger.info("Usage analytics: disabled")
return
demo_name = self._resolve_demo_name()
try:
log_dir = Path("usage_logs")
log_dir.mkdir(exist_ok=True)
self._log_file = log_dir / f"events_{uuid.uuid4().hex[:12]}.jsonl"
token = (
os.environ.get("ANALYTICS_TOKEN")
or os.environ.get("HF_TOKEN")
or os.environ.get("MODEL_ACCESS_TOKEN")
)
self._fh = self._log_file.open("a")
self._scheduler = _make_cleanup_scheduler(
active_log_file=self._log_file,
repo_id=repo_id,
repo_type="dataset",
folder_path=log_dir,
path_in_repo=demo_name,
every=push_interval_minutes,
token=token,
)
self._enabled = True
logger.info(
f"Usage analytics: enabled -> {repo_id}/{demo_name} "
f"(push every {push_interval_minutes}m)"
)
except Exception as exc:
logger.warning(f"Usage analytics: init failed, continuing without ({exc})")
@property
def enabled(self) -> bool:
return self._enabled
def log(self, session_hash: str, event: str, **details: str | int | float | bool) -> None:
"""Append a usage event. Thread-safe, never raises.
Args:
session_hash: Gradio session hash (truncated to 8 chars for privacy).
event: Event name (e.g. ``"session_start"``, ``"video_process"``).
**details: Arbitrary key-value pairs stored alongside the event.
"""
if not self._enabled or self._fh is None or self._scheduler is None:
return
try:
entry = {
"ts": datetime.now(timezone.utc).isoformat(),
"session": session_hash[:8] if session_hash else "unknown",
"event": event,
**details,
}
with self._scheduler.lock:
self._fh.write(json.dumps(entry, separators=(",", ":")) + "\n")
self._fh.flush()
except Exception:
pass
def shutdown(self) -> None:
"""Flush and close the log file handle."""
if self._fh is not None:
try:
self._fh.flush()
self._fh.close()
except Exception:
pass
self._fh = None
@staticmethod
def _resolve_demo_name() -> str:
"""Derive demo name from the HF Space name, or ``"debug-<folder>"`` locally.
The folder suffix keeps eve_hmi / eve_gmod / future demos from
sharing the same ``debug/`` path_in_repo when they're run locally.
"""
space_id = os.environ.get("SPACE_ID", "")
if "/" in space_id:
return space_id.split("/", 1)[1]
if space_id:
return space_id
try:
folder = Path(sys.argv[0]).resolve().parent.name
if folder:
return f"debug-{folder}"
except Exception:
pass
return "debug"
@staticmethod
def _resolve_enabled(explicit: bool | None) -> bool:
"""Determine whether analytics should be enabled.
Priority: USAGE_ANALYTICS env var > explicit param > SPACE_ID auto-detect.
"""
env = os.environ.get("USAGE_ANALYTICS", "").strip().lower()
if env in ("0", "false", "off"):
return False
if env in ("1", "true", "on"):
return True
if explicit is not None:
return explicit
return bool(os.environ.get("SPACE_ID"))
|