WasabiDrop Claude commited on
Commit
63bc49c
Β·
1 Parent(s): ab36c20

πŸ”₯ Replace SQLite with Firebase for persistent logging

Browse files

- Create new Firebase-based event logging system
- Replace SQLite databases with Firebase Realtime Database
- Add comprehensive analytics and structured logging
- Eliminate permission issues with file system access
- Ensure persistent logging across HF Space restarts
- Maintain backward compatibility with existing code
- Add graceful fallbacks when Firebase unavailable

Benefits:
- βœ… Persistent data survives HF Space restarts
- βœ… No file permission issues
- βœ… Scalable and reliable
- βœ… Real-time analytics
- βœ… Already configured and working

πŸ€– Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (2) hide show
  1. core/app.py +1 -2
  2. src/services/firebase_logger.py +369 -0
core/app.py CHANGED
@@ -30,8 +30,7 @@ from ai.tokenizers.unified_tokenizer import unified_tokenizer
30
  # Import authentication system
31
  from src.config.auth import auth_config
32
  from src.middleware.auth import require_auth, check_quota, track_token_usage
33
- from src.services.event_logger import event_logger
34
- from src.services.structured_event_logger import structured_logger
35
  from src.services.user_store import user_store
36
  from src.routes.admin import admin_bp
37
  from src.routes.admin_web import admin_web_bp
 
30
  # Import authentication system
31
  from src.config.auth import auth_config
32
  from src.middleware.auth import require_auth, check_quota, track_token_usage
33
+ from src.services.firebase_logger import event_logger, structured_logger
 
34
  from src.services.user_store import user_store
35
  from src.routes.admin import admin_bp
36
  from src.routes.admin_web import admin_web_bp
src/services/firebase_logger.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ πŸ”₯ Firebase-based Event Logging System for NyanProxy
3
+ ===================================================
4
+
5
+ This module provides a comprehensive event logging system using Firebase Realtime Database
6
+ for persistent, scalable logging across deployments.
7
+ """
8
+
9
+ import json
10
+ import threading
11
+ import time
12
+ from datetime import datetime, timedelta
13
+ from typing import Dict, List, Optional, Any, Union
14
+ from dataclasses import dataclass, asdict
15
+ from enum import Enum
16
+ import uuid
17
+ import hashlib
18
+
19
+ try:
20
+ import firebase_admin
21
+ from firebase_admin import credentials, db
22
+ FIREBASE_AVAILABLE = True
23
+ except ImportError:
24
+ FIREBASE_AVAILABLE = False
25
+ print("Warning: Firebase not available. Logging will be disabled.")
26
+
27
+ @dataclass
28
+ class Event:
29
+ """Simple event structure for basic logging"""
30
+ token: str
31
+ event_type: str
32
+ payload: Dict[str, Any]
33
+ timestamp: datetime = None
34
+
35
+ def __post_init__(self):
36
+ if self.timestamp is None:
37
+ self.timestamp = datetime.now()
38
+
39
+ class EventType(Enum):
40
+ """Event types for structured logging"""
41
+ CHAT_COMPLETION = "chat_completion"
42
+ STREAMING_REQUEST = "streaming_request"
43
+ MODEL_LIST_REQUEST = "model_list_request"
44
+ LOGIN_SUCCESS = "login_success"
45
+ LOGIN_FAILURE = "login_failure"
46
+ RATE_LIMIT_HIT = "rate_limit_hit"
47
+ API_ERROR = "api_error"
48
+ HEALTH_CHECK = "health_check"
49
+
50
+ class FirebaseEventLogger:
51
+ """
52
+ Firebase-based event logger for persistent logging
53
+ """
54
+
55
+ def __init__(self, firebase_path: str = "events"):
56
+ self.firebase_path = firebase_path
57
+ self.lock = threading.Lock()
58
+ self.firebase_available = FIREBASE_AVAILABLE
59
+
60
+ if not self.firebase_available:
61
+ print("Warning: Firebase not available. Events will not be logged.")
62
+ return
63
+
64
+ try:
65
+ # Test Firebase connection
66
+ self.db_ref = db.reference(self.firebase_path)
67
+ self.db_ref.child("test").set({"initialized": True, "timestamp": datetime.now().isoformat()})
68
+ print(f"Firebase logging initialized at path: {self.firebase_path}")
69
+ except Exception as e:
70
+ print(f"Warning: Firebase logging failed to initialize: {e}")
71
+ self.firebase_available = False
72
+
73
+ def log_event(self, event: Event) -> bool:
74
+ """Log an event to Firebase"""
75
+ if not self.firebase_available:
76
+ return False
77
+
78
+ try:
79
+ with self.lock:
80
+ event_data = {
81
+ "token": event.token,
82
+ "event_type": event.event_type,
83
+ "payload": event.payload,
84
+ "timestamp": event.timestamp.isoformat() if event.timestamp else datetime.now().isoformat()
85
+ }
86
+
87
+ # Use timestamp-based key for ordering
88
+ timestamp_key = int(time.time() * 1000) # milliseconds
89
+ event_key = f"{timestamp_key}_{uuid.uuid4().hex[:8]}"
90
+
91
+ self.db_ref.child(event_key).set(event_data)
92
+ return True
93
+
94
+ except Exception as e:
95
+ print(f"Failed to log event to Firebase: {e}")
96
+ return False
97
+
98
+ def log_chat_completion(self, token: str, model_family: str, input_tokens: int,
99
+ output_tokens: int, ip_hash: str = "", success: bool = True) -> bool:
100
+ """Log a chat completion event"""
101
+ event = Event(
102
+ token=token,
103
+ event_type="chat_completion",
104
+ payload={
105
+ "model_family": model_family,
106
+ "input_tokens": input_tokens,
107
+ "output_tokens": output_tokens,
108
+ "total_tokens": input_tokens + output_tokens,
109
+ "ip_hash": ip_hash,
110
+ "success": success
111
+ }
112
+ )
113
+ return self.log_event(event)
114
+
115
+ def log_api_request(self, endpoint: str, method: str, status_code: int,
116
+ response_time: float, token: str = None) -> bool:
117
+ """Log an API request event"""
118
+ event = Event(
119
+ token=token or "anonymous",
120
+ event_type="api_request",
121
+ payload={
122
+ "endpoint": endpoint,
123
+ "method": method,
124
+ "status_code": status_code,
125
+ "response_time": response_time,
126
+ "success": status_code < 400
127
+ }
128
+ )
129
+ return self.log_event(event)
130
+
131
+ def get_user_stats(self, token: str, days: int = 30) -> Dict[str, Any]:
132
+ """Get usage statistics for a user"""
133
+ if not self.firebase_available:
134
+ return {}
135
+
136
+ try:
137
+ # Get events from last N days
138
+ cutoff_time = datetime.now() - timedelta(days=days)
139
+ cutoff_timestamp = int(cutoff_time.timestamp() * 1000)
140
+
141
+ events = self.db_ref.order_by_key().start_at(str(cutoff_timestamp)).get()
142
+
143
+ if not events:
144
+ return {}
145
+
146
+ stats = {
147
+ "total_requests": 0,
148
+ "total_tokens": 0,
149
+ "input_tokens": 0,
150
+ "output_tokens": 0,
151
+ "chat_completions": 0,
152
+ "api_errors": 0,
153
+ "model_families": {}
154
+ }
155
+
156
+ for event_key, event_data in events.items():
157
+ if event_data.get("token") == token:
158
+ payload = event_data.get("payload", {})
159
+ event_type = event_data.get("event_type")
160
+
161
+ stats["total_requests"] += 1
162
+
163
+ if event_type == "chat_completion":
164
+ stats["chat_completions"] += 1
165
+ stats["input_tokens"] += payload.get("input_tokens", 0)
166
+ stats["output_tokens"] += payload.get("output_tokens", 0)
167
+ stats["total_tokens"] += payload.get("total_tokens", 0)
168
+
169
+ model_family = payload.get("model_family", "unknown")
170
+ if model_family not in stats["model_families"]:
171
+ stats["model_families"][model_family] = 0
172
+ stats["model_families"][model_family] += 1
173
+
174
+ elif event_type == "api_request" and not payload.get("success", True):
175
+ stats["api_errors"] += 1
176
+
177
+ return stats
178
+
179
+ except Exception as e:
180
+ print(f"Failed to get user stats: {e}")
181
+ return {}
182
+
183
+ def cleanup_old_events(self, days: int = 90) -> bool:
184
+ """Clean up events older than specified days"""
185
+ if not self.firebase_available:
186
+ return False
187
+
188
+ try:
189
+ cutoff_time = datetime.now() - timedelta(days=days)
190
+ cutoff_timestamp = int(cutoff_time.timestamp() * 1000)
191
+
192
+ # Get old events
193
+ old_events = self.db_ref.order_by_key().end_at(str(cutoff_timestamp)).get()
194
+
195
+ if old_events:
196
+ # Delete old events in batches
197
+ batch_size = 100
198
+ deleted_count = 0
199
+
200
+ for event_key in list(old_events.keys()):
201
+ self.db_ref.child(event_key).delete()
202
+ deleted_count += 1
203
+
204
+ if deleted_count % batch_size == 0:
205
+ time.sleep(0.1) # Small delay to avoid rate limits
206
+
207
+ print(f"Cleaned up {deleted_count} old events from Firebase")
208
+ return True
209
+
210
+ return True
211
+
212
+ except Exception as e:
213
+ print(f"Failed to cleanup old events: {e}")
214
+ return False
215
+
216
+ class FirebaseStructuredLogger:
217
+ """
218
+ Advanced Firebase-based structured logger with detailed analytics
219
+ """
220
+
221
+ def __init__(self, firebase_path: str = "structured_events"):
222
+ self.firebase_path = firebase_path
223
+ self.lock = threading.Lock()
224
+ self.firebase_available = FIREBASE_AVAILABLE
225
+
226
+ if not self.firebase_available:
227
+ print("Warning: Firebase not available. Structured logging will be disabled.")
228
+ return
229
+
230
+ try:
231
+ # Test Firebase connection
232
+ self.db_ref = db.reference(self.firebase_path)
233
+ self.db_ref.child("test").set({"initialized": True, "timestamp": datetime.now().isoformat()})
234
+ print(f"Firebase structured logging initialized at path: {self.firebase_path}")
235
+ except Exception as e:
236
+ print(f"Warning: Firebase structured logging failed to initialize: {e}")
237
+ self.firebase_available = False
238
+
239
+ def log_chat_completion(self, user_token: str, model_family: str, model_name: str,
240
+ input_tokens: int, output_tokens: int, cost_usd: float,
241
+ response_time_ms: float, success: bool, ip_hash: str = "",
242
+ user_agent: str = "") -> bool:
243
+ """Log a detailed chat completion event"""
244
+ if not self.firebase_available:
245
+ return False
246
+
247
+ try:
248
+ with self.lock:
249
+ event_data = {
250
+ "event_type": "chat_completion",
251
+ "timestamp": datetime.now().isoformat(),
252
+ "user_token": user_token,
253
+ "model_family": model_family,
254
+ "model_name": model_name,
255
+ "input_tokens": input_tokens,
256
+ "output_tokens": output_tokens,
257
+ "total_tokens": input_tokens + output_tokens,
258
+ "cost_usd": cost_usd,
259
+ "response_time_ms": response_time_ms,
260
+ "success": success,
261
+ "ip_hash": ip_hash,
262
+ "user_agent": user_agent
263
+ }
264
+
265
+ # Use timestamp-based key for ordering
266
+ timestamp_key = int(time.time() * 1000) # milliseconds
267
+ event_key = f"{timestamp_key}_{uuid.uuid4().hex[:8]}"
268
+
269
+ self.db_ref.child(event_key).set(event_data)
270
+ return True
271
+
272
+ except Exception as e:
273
+ print(f"Failed to log structured event to Firebase: {e}")
274
+ return False
275
+
276
+ def get_analytics(self, days: int = 30) -> Dict[str, Any]:
277
+ """Get comprehensive analytics from Firebase"""
278
+ if not self.firebase_available:
279
+ return {}
280
+
281
+ try:
282
+ # Get events from last N days
283
+ cutoff_time = datetime.now() - timedelta(days=days)
284
+ cutoff_timestamp = int(cutoff_time.timestamp() * 1000)
285
+
286
+ events = self.db_ref.order_by_key().start_at(str(cutoff_timestamp)).get()
287
+
288
+ if not events:
289
+ return {}
290
+
291
+ analytics = {
292
+ "total_requests": len(events),
293
+ "total_tokens": 0,
294
+ "total_cost": 0.0,
295
+ "avg_response_time": 0.0,
296
+ "success_rate": 0.0,
297
+ "model_usage": {},
298
+ "user_activity": {},
299
+ "hourly_distribution": {}
300
+ }
301
+
302
+ successful_requests = 0
303
+ total_response_time = 0
304
+
305
+ for event_key, event_data in events.items():
306
+ if event_data.get("event_type") == "chat_completion":
307
+ analytics["total_tokens"] += event_data.get("total_tokens", 0)
308
+ analytics["total_cost"] += event_data.get("cost_usd", 0)
309
+
310
+ if event_data.get("success", False):
311
+ successful_requests += 1
312
+ total_response_time += event_data.get("response_time_ms", 0)
313
+
314
+ # Model usage
315
+ model = event_data.get("model_name", "unknown")
316
+ if model not in analytics["model_usage"]:
317
+ analytics["model_usage"][model] = 0
318
+ analytics["model_usage"][model] += 1
319
+
320
+ # User activity
321
+ user = event_data.get("user_token", "anonymous")
322
+ if user not in analytics["user_activity"]:
323
+ analytics["user_activity"][user] = 0
324
+ analytics["user_activity"][user] += 1
325
+
326
+ # Hourly distribution
327
+ timestamp = event_data.get("timestamp", "")
328
+ if timestamp:
329
+ try:
330
+ dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
331
+ hour = dt.hour
332
+ if hour not in analytics["hourly_distribution"]:
333
+ analytics["hourly_distribution"][hour] = 0
334
+ analytics["hourly_distribution"][hour] += 1
335
+ except:
336
+ pass
337
+
338
+ # Calculate averages
339
+ if successful_requests > 0:
340
+ analytics["avg_response_time"] = total_response_time / successful_requests
341
+ analytics["success_rate"] = successful_requests / len(events)
342
+
343
+ return analytics
344
+
345
+ except Exception as e:
346
+ print(f"Failed to get analytics: {e}")
347
+ return {}
348
+
349
+ # Global logger instances
350
+ firebase_event_logger = FirebaseEventLogger()
351
+ firebase_structured_logger = FirebaseStructuredLogger()
352
+
353
+ # Backward compatibility - use Firebase loggers if available, otherwise create dummy loggers
354
+ if FIREBASE_AVAILABLE:
355
+ # Replace the old loggers with Firebase ones
356
+ event_logger = firebase_event_logger
357
+ structured_logger = firebase_structured_logger
358
+ else:
359
+ # Create dummy loggers that don't crash the app
360
+ class DummyLogger:
361
+ def log_event(self, *args, **kwargs): return False
362
+ def log_chat_completion(self, *args, **kwargs): return False
363
+ def log_api_request(self, *args, **kwargs): return False
364
+ def get_user_stats(self, *args, **kwargs): return {}
365
+ def cleanup_old_events(self, *args, **kwargs): return False
366
+ def get_analytics(self, *args, **kwargs): return {}
367
+
368
+ event_logger = DummyLogger()
369
+ structured_logger = DummyLogger()