Mirrowel commited on
Commit
4cdd261
·
1 Parent(s): 3c52746

feat(usage): ✨ add human-readable timestamp fields to usage data for debugging

Browse files

This commit introduces helper methods to automatically generate and persist human-readable timestamp fields alongside Unix timestamps in the usage tracking data.

- Add `_format_timestamp_local()` method to convert Unix timestamps to local time strings with timezone offset
- Add `_add_readable_timestamps()` method to enrich usage data with 'window_started' and 'quota_resets' fields
- Integrate timestamp formatting into the save flow, automatically updating readable fields before persisting to disk
- Set `quota_reset_ts` when initializing new model windows based on provider's window configuration

The readable timestamps improve observability and debugging by making it easier to understand when quota windows started and when they will reset, without requiring manual timestamp conversion.

src/rotator_library/usage_manager.py CHANGED
@@ -297,6 +297,69 @@ class UsageManager:
297
  .get("success_count", 0)
298
  )
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  def _select_sequential(
301
  self,
302
  candidates: List[Tuple[str, int]],
@@ -377,6 +440,8 @@ class UsageManager:
377
  if self._usage_data is None:
378
  return
379
  async with self._data_lock:
 
 
380
  async with aiofiles.open(self.file_path, "w") as f:
381
  await f.write(json.dumps(self._usage_data, indent=2))
382
 
@@ -1251,11 +1316,15 @@ class UsageManager:
1251
  # Start window on first request for this model
1252
  if model_data.get("window_start_ts") is None:
1253
  model_data["window_start_ts"] = now_ts
1254
- window_hours = (
1255
- reset_config.get("window_seconds", 0) / 3600
1256
- if reset_config
1257
- else 0
1258
  )
 
 
 
 
1259
  lib_logger.info(
1260
  f"Started {window_hours:.1f}h window for model {model} on {mask_credential(key)}"
1261
  )
 
297
  .get("success_count", 0)
298
  )
299
 
300
+ # =========================================================================
301
+ # TIMESTAMP FORMATTING HELPERS
302
+ # =========================================================================
303
+
304
+ def _format_timestamp_local(self, ts: Optional[float]) -> Optional[str]:
305
+ """
306
+ Format Unix timestamp as local time string with timezone offset.
307
+
308
+ Args:
309
+ ts: Unix timestamp or None
310
+
311
+ Returns:
312
+ Formatted string like "2025-12-07 14:30:17 +0100" or None
313
+ """
314
+ if ts is None:
315
+ return None
316
+ try:
317
+ dt = datetime.fromtimestamp(ts).astimezone() # Local timezone
318
+ # Use UTC offset for conciseness (works on all platforms)
319
+ return dt.strftime("%Y-%m-%d %H:%M:%S %z")
320
+ except (OSError, ValueError, OverflowError):
321
+ return None
322
+
323
+ def _add_readable_timestamps(self, data: Dict) -> Dict:
324
+ """
325
+ Add human-readable timestamp fields to usage data before saving.
326
+
327
+ Adds 'window_started' and 'quota_resets' fields derived from
328
+ Unix timestamps for easier debugging and monitoring.
329
+
330
+ Args:
331
+ data: The usage data dict to enhance
332
+
333
+ Returns:
334
+ The same dict with readable timestamp fields added
335
+ """
336
+ for key, key_data in data.items():
337
+ # Handle per-model structure
338
+ models = key_data.get("models", {})
339
+ for model_name, model_stats in models.items():
340
+ if not isinstance(model_stats, dict):
341
+ continue
342
+
343
+ # Add readable window start time
344
+ window_start = model_stats.get("window_start_ts")
345
+ if window_start:
346
+ model_stats["window_started"] = self._format_timestamp_local(
347
+ window_start
348
+ )
349
+ elif "window_started" in model_stats:
350
+ del model_stats["window_started"]
351
+
352
+ # Add readable reset time
353
+ quota_reset = model_stats.get("quota_reset_ts")
354
+ if quota_reset:
355
+ model_stats["quota_resets"] = self._format_timestamp_local(
356
+ quota_reset
357
+ )
358
+ elif "quota_resets" in model_stats:
359
+ del model_stats["quota_resets"]
360
+
361
+ return data
362
+
363
  def _select_sequential(
364
  self,
365
  candidates: List[Tuple[str, int]],
 
440
  if self._usage_data is None:
441
  return
442
  async with self._data_lock:
443
+ # Add human-readable timestamp fields before saving
444
+ self._add_readable_timestamps(self._usage_data)
445
  async with aiofiles.open(self.file_path, "w") as f:
446
  await f.write(json.dumps(self._usage_data, indent=2))
447
 
 
1316
  # Start window on first request for this model
1317
  if model_data.get("window_start_ts") is None:
1318
  model_data["window_start_ts"] = now_ts
1319
+
1320
+ # Set expected quota reset time from provider config
1321
+ window_seconds = (
1322
+ reset_config.get("window_seconds", 0) if reset_config else 0
1323
  )
1324
+ if window_seconds > 0:
1325
+ model_data["quota_reset_ts"] = now_ts + window_seconds
1326
+
1327
+ window_hours = window_seconds / 3600 if window_seconds else 0
1328
  lib_logger.info(
1329
  f"Started {window_hours:.1f}h window for model {model} on {mask_credential(key)}"
1330
  )