llm-cost-guard 0.1.2__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/PKG-INFO +76 -1
  2. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/README.md +75 -0
  3. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/__init__.py +16 -1
  4. llm_cost_guard-0.2.0/llm_cost_guard/audit.py +480 -0
  5. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/backends/__init__.py +1 -1
  6. llm_cost_guard-0.2.0/llm_cost_guard/backends/redis_backend.py +557 -0
  7. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/tracker.py +114 -3
  8. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/pyproject.toml +1 -1
  9. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/.github/workflows/ci.yml +0 -0
  10. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/.github/workflows/publish.yml +0 -0
  11. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/.gitignore +0 -0
  12. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/LICENSE +0 -0
  13. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/examples/bedrock_example.py +0 -0
  14. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/examples/budget_alerts.py +0 -0
  15. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/examples/distributed_tracking.py +0 -0
  16. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/examples/langchain_rag.py +0 -0
  17. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/examples/quickstart.py +0 -0
  18. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/examples/streaming_example.py +0 -0
  19. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/backends/base.py +0 -0
  20. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/backends/memory.py +0 -0
  21. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/backends/sqlite.py +0 -0
  22. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/budget.py +0 -0
  23. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/cli.py +0 -0
  24. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/clients/__init__.py +0 -0
  25. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/clients/anthropic.py +0 -0
  26. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/clients/openai.py +0 -0
  27. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/exceptions.py +0 -0
  28. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/integrations/__init__.py +0 -0
  29. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/integrations/cache.py +0 -0
  30. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/integrations/langchain.py +0 -0
  31. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/models.py +0 -0
  32. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/pricing/__init__.py +0 -0
  33. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/pricing/anthropic.yaml +0 -0
  34. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/pricing/bedrock.yaml +0 -0
  35. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/pricing/loader.py +0 -0
  36. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/pricing/openai.yaml +0 -0
  37. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/pricing/vertex.yaml +0 -0
  38. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/providers/__init__.py +0 -0
  39. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/providers/anthropic.py +0 -0
  40. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/providers/base.py +0 -0
  41. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/providers/bedrock.py +0 -0
  42. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/providers/openai.py +0 -0
  43. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/rate_limit.py +0 -0
  44. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/span.py +0 -0
  45. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/tokenizers/__init__.py +0 -0
  46. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/llm_cost_guard/tokenizers/base.py +0 -0
  47. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/__init__.py +0 -0
  48. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/conftest.py +0 -0
  49. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/integration/__init__.py +0 -0
  50. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/integration/test_e2e.py +0 -0
  51. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/__init__.py +0 -0
  52. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/test_backends.py +0 -0
  53. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/test_budget.py +0 -0
  54. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/test_pricing.py +0 -0
  55. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/test_providers.py +0 -0
  56. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/test_rate_limit.py +0 -0
  57. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/test_span.py +0 -0
  58. {llm_cost_guard-0.1.2 → llm_cost_guard-0.2.0}/tests/unit/test_tracker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llm-cost-guard
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: Real-time cost tracking, budget enforcement, and usage analytics for LLM applications
5
5
  Project-URL: Homepage, https://github.com/prashantdudami/llm-cost-guard
6
6
  Project-URL: Documentation, https://github.com/prashantdudami/llm-cost-guard#readme
@@ -334,6 +334,57 @@ tracker = CostTracker(
334
334
  )
335
335
  ```
336
336
 
337
+ ## Audit Logging (v0.2.0+)
338
+
339
+ Enterprise-ready audit trails for compliance:
340
+
341
+ ```python
342
+ from llm_cost_guard import CostTracker, FileAuditBackend
343
+
344
+ # Enable audit logging
345
+ tracker = CostTracker(
346
+ audit_enabled=True,
347
+ audit_backend=FileAuditBackend("audit.log"),
348
+ )
349
+
350
+ # Query audit history
351
+ events = tracker.audit.query(
352
+ event_type=AuditEventType.BUDGET_EXCEEDED,
353
+ start_date="2024-01-01",
354
+ )
355
+
356
+ # Get budget-specific history
357
+ history = tracker.audit.get_budget_history("daily")
358
+ ```
359
+
360
+ Audit events include:
361
+ - Budget created/modified/deleted
362
+ - Budget warnings and exceeded events
363
+ - Rate limit exceeded events
364
+ - Tracking failures and fallback activations
365
+
366
+ ## Observability Metrics (v0.2.0+)
367
+
368
+ Track health and degradation:
369
+
370
+ ```python
371
+ # Get tracker metrics
372
+ metrics = tracker.get_metrics()
373
+ print(metrics)
374
+ # {
375
+ # "backend_failures": 0,
376
+ # "fallback_activations": 0,
377
+ # "budget_exceeded_count": 3,
378
+ # "tracking_errors": 0,
379
+ # "using_fallback": False,
380
+ # }
381
+
382
+ # Health check
383
+ health = tracker.health_check()
384
+ print(health.healthy) # True/False
385
+ print(health.errors) # List of issues
386
+ ```
387
+
337
388
  ## Custom Pricing
338
389
 
339
390
  For negotiated enterprise rates:
@@ -349,6 +400,30 @@ tracker = CostTracker(
349
400
  )
350
401
  ```
351
402
 
403
+ ## Current Limitations
404
+
405
+ Being transparent about what's not yet production-ready:
406
+
407
+ | Feature | Status | Notes |
408
+ |---------|--------|-------|
409
+ | Distributed budgets (Redis) | ✅ v0.2.0 | Atomic operations with Lua scripts |
410
+ | Audit logging | ✅ v0.2.0 | File and logging backends |
411
+ | Graceful degradation metrics | ✅ v0.2.0 | Track failures and fallbacks |
412
+ | PostgreSQL backend | 🚧 Planned | Use SQLite or Redis for now |
413
+ | DynamoDB backend | 🚧 Planned | Use SQLite or Redis for now |
414
+ | Encryption at rest | 🚧 Planned | Use encrypted volumes as workaround |
415
+ | Multi-tenancy optimization | 🚧 Planned | Use tag-scoped budgets for now |
416
+ | Streaming cost estimation | ⚠️ Limited | Actual cost tracked on completion |
417
+ | Fine-tuning cost tracking | ❌ Not supported | |
418
+
419
+ ### Recommended for Production
420
+
421
+ | Deployment Size | Backend | Notes |
422
+ |-----------------|---------|-------|
423
+ | Single instance | SQLite | Simple, no setup |
424
+ | Multiple instances | Redis | Distributed budget enforcement |
425
+ | High-volume (>1k req/s) | Redis | With sampling (coming soon) |
426
+
352
427
  ## Contributing
353
428
 
354
429
  Contributions are welcome! Please read our contributing guidelines and submit pull requests.
@@ -271,6 +271,57 @@ tracker = CostTracker(
271
271
  )
272
272
  ```
273
273
 
274
+ ## Audit Logging (v0.2.0+)
275
+
276
+ Enterprise-ready audit trails for compliance:
277
+
278
+ ```python
279
+ from llm_cost_guard import CostTracker, FileAuditBackend
280
+
281
+ # Enable audit logging
282
+ tracker = CostTracker(
283
+ audit_enabled=True,
284
+ audit_backend=FileAuditBackend("audit.log"),
285
+ )
286
+
287
+ # Query audit history
288
+ events = tracker.audit.query(
289
+ event_type=AuditEventType.BUDGET_EXCEEDED,
290
+ start_date="2024-01-01",
291
+ )
292
+
293
+ # Get budget-specific history
294
+ history = tracker.audit.get_budget_history("daily")
295
+ ```
296
+
297
+ Audit events include:
298
+ - Budget created/modified/deleted
299
+ - Budget warnings and exceeded events
300
+ - Rate limit exceeded events
301
+ - Tracking failures and fallback activations
302
+
303
+ ## Observability Metrics (v0.2.0+)
304
+
305
+ Track health and degradation:
306
+
307
+ ```python
308
+ # Get tracker metrics
309
+ metrics = tracker.get_metrics()
310
+ print(metrics)
311
+ # {
312
+ # "backend_failures": 0,
313
+ # "fallback_activations": 0,
314
+ # "budget_exceeded_count": 3,
315
+ # "tracking_errors": 0,
316
+ # "using_fallback": False,
317
+ # }
318
+
319
+ # Health check
320
+ health = tracker.health_check()
321
+ print(health.healthy) # True/False
322
+ print(health.errors) # List of issues
323
+ ```
324
+
274
325
  ## Custom Pricing
275
326
 
276
327
  For negotiated enterprise rates:
@@ -286,6 +337,30 @@ tracker = CostTracker(
286
337
  )
287
338
  ```
288
339
 
340
+ ## Current Limitations
341
+
342
+ Being transparent about what's not yet production-ready:
343
+
344
+ | Feature | Status | Notes |
345
+ |---------|--------|-------|
346
+ | Distributed budgets (Redis) | ✅ v0.2.0 | Atomic operations with Lua scripts |
347
+ | Audit logging | ✅ v0.2.0 | File and logging backends |
348
+ | Graceful degradation metrics | ✅ v0.2.0 | Track failures and fallbacks |
349
+ | PostgreSQL backend | 🚧 Planned | Use SQLite or Redis for now |
350
+ | DynamoDB backend | 🚧 Planned | Use SQLite or Redis for now |
351
+ | Encryption at rest | 🚧 Planned | Use encrypted volumes as workaround |
352
+ | Multi-tenancy optimization | 🚧 Planned | Use tag-scoped budgets for now |
353
+ | Streaming cost estimation | ⚠️ Limited | Actual cost tracked on completion |
354
+ | Fine-tuning cost tracking | ❌ Not supported | |
355
+
356
+ ### Recommended for Production
357
+
358
+ | Deployment Size | Backend | Notes |
359
+ |-----------------|---------|-------|
360
+ | Single instance | SQLite | Simple, no setup |
361
+ | Multiple instances | Redis | Distributed budget enforcement |
362
+ | High-volume (>1k req/s) | Redis | With sampling (coming soon) |
363
+
289
364
  ## Contributing
290
365
 
291
366
  Contributions are welcome! Please read our contributing guidelines and submit pull requests.
@@ -15,8 +15,16 @@ from llm_cost_guard.exceptions import (
15
15
  TrackingUnavailableError,
16
16
  RateLimitExceededError,
17
17
  )
18
+ from llm_cost_guard.audit import (
19
+ AuditLogger,
20
+ AuditBackend,
21
+ AuditEvent,
22
+ AuditEventType,
23
+ LoggingAuditBackend,
24
+ FileAuditBackend,
25
+ )
18
26
 
19
- __version__ = "0.1.0"
27
+ __version__ = "0.2.0"
20
28
 
21
29
  __all__ = [
22
30
  # Core
@@ -29,6 +37,13 @@ __all__ = [
29
37
  "CostRecord",
30
38
  "CostReport",
31
39
  "HealthStatus",
40
+ # Audit
41
+ "AuditLogger",
42
+ "AuditBackend",
43
+ "AuditEvent",
44
+ "AuditEventType",
45
+ "LoggingAuditBackend",
46
+ "FileAuditBackend",
32
47
  # Exceptions
33
48
  "LLMCostGuardError",
34
49
  "BudgetExceededError",
@@ -0,0 +1,480 @@
1
+ """
2
+ Audit logging for LLM Cost Guard.
3
+
4
+ Provides compliance-ready audit trails for:
5
+ - Budget changes (created, modified, deleted)
6
+ - Budget exceeded events
7
+ - Configuration changes
8
+ - Rate limit events
9
+ """
10
+
11
+ import json
12
+ import logging
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field, asdict
15
+ from datetime import datetime
16
+ from enum import Enum
17
+ from typing import Any, Dict, List, Optional, Callable
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class AuditEventType(str, Enum):
23
+ """Types of audit events."""
24
+
25
+ # Budget events
26
+ BUDGET_CREATED = "budget.created"
27
+ BUDGET_MODIFIED = "budget.modified"
28
+ BUDGET_DELETED = "budget.deleted"
29
+ BUDGET_WARNING = "budget.warning"
30
+ BUDGET_EXCEEDED = "budget.exceeded"
31
+ BUDGET_RESET = "budget.reset"
32
+
33
+ # Rate limit events
34
+ RATE_LIMIT_EXCEEDED = "rate_limit.exceeded"
35
+
36
+ # Configuration events
37
+ CONFIG_CHANGED = "config.changed"
38
+ BACKEND_CHANGED = "backend.changed"
39
+ PRICING_UPDATED = "pricing.updated"
40
+
41
+ # Tracking events
42
+ TRACKING_FAILURE = "tracking.failure"
43
+ FALLBACK_ACTIVATED = "fallback.activated"
44
+
45
+ # Cost events
46
+ COST_RECORDED = "cost.recorded"
47
+ COST_ANOMALY = "cost.anomaly"
48
+
49
+
50
+ @dataclass
51
+ class AuditEvent:
52
+ """Represents an audit event."""
53
+
54
+ event_type: AuditEventType
55
+ timestamp: datetime = field(default_factory=datetime.now)
56
+ actor: Optional[str] = None # Who initiated the action (user/system)
57
+ resource: Optional[str] = None # What was affected (budget name, etc.)
58
+ details: Dict[str, Any] = field(default_factory=dict)
59
+ metadata: Dict[str, Any] = field(default_factory=dict)
60
+
61
+ def to_dict(self) -> Dict[str, Any]:
62
+ """Convert to dictionary for serialization."""
63
+ return {
64
+ "event_type": self.event_type.value,
65
+ "timestamp": self.timestamp.isoformat(),
66
+ "actor": self.actor,
67
+ "resource": self.resource,
68
+ "details": self.details,
69
+ "metadata": self.metadata,
70
+ }
71
+
72
+ def to_json(self) -> str:
73
+ """Convert to JSON string."""
74
+ return json.dumps(self.to_dict())
75
+
76
+
77
+ class AuditBackend(ABC):
78
+ """Abstract base class for audit backends."""
79
+
80
+ @abstractmethod
81
+ def log(self, event: AuditEvent) -> None:
82
+ """Log an audit event."""
83
+ pass
84
+
85
+ @abstractmethod
86
+ def query(
87
+ self,
88
+ event_type: Optional[AuditEventType] = None,
89
+ start_date: Optional[datetime] = None,
90
+ end_date: Optional[datetime] = None,
91
+ resource: Optional[str] = None,
92
+ limit: int = 100,
93
+ ) -> List[AuditEvent]:
94
+ """Query audit events."""
95
+ pass
96
+
97
+
98
+ class LoggingAuditBackend(AuditBackend):
99
+ """
100
+ Audit backend that logs to Python logging.
101
+
102
+ Suitable for development and when audit logs should go to
103
+ existing log aggregation systems (CloudWatch, DataDog, etc.).
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ logger_name: str = "llm_cost_guard.audit",
109
+ log_level: int = logging.INFO,
110
+ ):
111
+ self._logger = logging.getLogger(logger_name)
112
+ self._log_level = log_level
113
+ self._events: List[AuditEvent] = [] # In-memory for query support
114
+ self._max_events = 10000 # Limit in-memory storage
115
+
116
+ def log(self, event: AuditEvent) -> None:
117
+ """Log an audit event."""
118
+ # Log to Python logger
119
+ self._logger.log(
120
+ self._log_level,
121
+ f"AUDIT: {event.event_type.value} | resource={event.resource} | {event.details}"
122
+ )
123
+
124
+ # Store in memory for queries
125
+ self._events.append(event)
126
+
127
+ # Trim if too many events
128
+ if len(self._events) > self._max_events:
129
+ self._events = self._events[-self._max_events:]
130
+
131
+ def query(
132
+ self,
133
+ event_type: Optional[AuditEventType] = None,
134
+ start_date: Optional[datetime] = None,
135
+ end_date: Optional[datetime] = None,
136
+ resource: Optional[str] = None,
137
+ limit: int = 100,
138
+ ) -> List[AuditEvent]:
139
+ """Query audit events."""
140
+ results = []
141
+
142
+ for event in reversed(self._events): # Most recent first
143
+ if event_type and event.event_type != event_type:
144
+ continue
145
+ if start_date and event.timestamp < start_date:
146
+ continue
147
+ if end_date and event.timestamp > end_date:
148
+ continue
149
+ if resource and event.resource != resource:
150
+ continue
151
+
152
+ results.append(event)
153
+
154
+ if len(results) >= limit:
155
+ break
156
+
157
+ return results
158
+
159
+
160
+ class FileAuditBackend(AuditBackend):
161
+ """
162
+ Audit backend that writes to a file (JSON Lines format).
163
+
164
+ Suitable for compliance requirements where audit logs need
165
+ to be stored in a specific location.
166
+ """
167
+
168
+ def __init__(self, file_path: str):
169
+ self._file_path = file_path
170
+
171
+ def log(self, event: AuditEvent) -> None:
172
+ """Log an audit event."""
173
+ with open(self._file_path, "a") as f:
174
+ f.write(event.to_json() + "\n")
175
+
176
+ def query(
177
+ self,
178
+ event_type: Optional[AuditEventType] = None,
179
+ start_date: Optional[datetime] = None,
180
+ end_date: Optional[datetime] = None,
181
+ resource: Optional[str] = None,
182
+ limit: int = 100,
183
+ ) -> List[AuditEvent]:
184
+ """Query audit events from file."""
185
+ results = []
186
+
187
+ try:
188
+ with open(self._file_path, "r") as f:
189
+ lines = f.readlines()
190
+ except FileNotFoundError:
191
+ return []
192
+
193
+ for line in reversed(lines): # Most recent first
194
+ if not line.strip():
195
+ continue
196
+
197
+ data = json.loads(line)
198
+ event = AuditEvent(
199
+ event_type=AuditEventType(data["event_type"]),
200
+ timestamp=datetime.fromisoformat(data["timestamp"]),
201
+ actor=data.get("actor"),
202
+ resource=data.get("resource"),
203
+ details=data.get("details", {}),
204
+ metadata=data.get("metadata", {}),
205
+ )
206
+
207
+ if event_type and event.event_type != event_type:
208
+ continue
209
+ if start_date and event.timestamp < start_date:
210
+ continue
211
+ if end_date and event.timestamp > end_date:
212
+ continue
213
+ if resource and event.resource != resource:
214
+ continue
215
+
216
+ results.append(event)
217
+
218
+ if len(results) >= limit:
219
+ break
220
+
221
+ return results
222
+
223
+
224
+ class CompositeAuditBackend(AuditBackend):
225
+ """
226
+ Audit backend that writes to multiple backends.
227
+
228
+ Useful for sending audit logs to both local storage and remote systems.
229
+ """
230
+
231
+ def __init__(self, backends: List[AuditBackend]):
232
+ self._backends = backends
233
+
234
+ def log(self, event: AuditEvent) -> None:
235
+ """Log an audit event to all backends."""
236
+ for backend in self._backends:
237
+ try:
238
+ backend.log(event)
239
+ except Exception as e:
240
+ logger.error(f"Audit backend failed: {e}")
241
+
242
+ def query(
243
+ self,
244
+ event_type: Optional[AuditEventType] = None,
245
+ start_date: Optional[datetime] = None,
246
+ end_date: Optional[datetime] = None,
247
+ resource: Optional[str] = None,
248
+ limit: int = 100,
249
+ ) -> List[AuditEvent]:
250
+ """Query from first backend that succeeds."""
251
+ for backend in self._backends:
252
+ try:
253
+ return backend.query(event_type, start_date, end_date, resource, limit)
254
+ except Exception as e:
255
+ logger.warning(f"Audit query failed on backend: {e}")
256
+ return []
257
+
258
+
259
+ class AuditLogger:
260
+ """
261
+ Main audit logger for LLM Cost Guard.
262
+
263
+ Usage:
264
+ audit = AuditLogger(backend=FileAuditBackend("audit.log"))
265
+
266
+ audit.log_budget_created(budget)
267
+ audit.log_budget_exceeded(budget, current_spending)
268
+ """
269
+
270
+ def __init__(
271
+ self,
272
+ backend: Optional[AuditBackend] = None,
273
+ enabled: bool = True,
274
+ actor: Optional[str] = None,
275
+ ):
276
+ self._backend = backend or LoggingAuditBackend()
277
+ self._enabled = enabled
278
+ self._default_actor = actor or "system"
279
+ self._event_callbacks: Dict[AuditEventType, List[Callable[[AuditEvent], None]]] = {}
280
+
281
+ def _log(self, event: AuditEvent) -> None:
282
+ """Internal logging method."""
283
+ if not self._enabled:
284
+ return
285
+
286
+ if event.actor is None:
287
+ event.actor = self._default_actor
288
+
289
+ self._backend.log(event)
290
+
291
+ # Trigger callbacks
292
+ callbacks = self._event_callbacks.get(event.event_type, [])
293
+ for callback in callbacks:
294
+ try:
295
+ callback(event)
296
+ except Exception as e:
297
+ logger.error(f"Audit callback failed: {e}")
298
+
299
+ def on_event(
300
+ self,
301
+ event_type: AuditEventType,
302
+ callback: Callable[[AuditEvent], None],
303
+ ) -> None:
304
+ """Register a callback for an event type."""
305
+ if event_type not in self._event_callbacks:
306
+ self._event_callbacks[event_type] = []
307
+ self._event_callbacks[event_type].append(callback)
308
+
309
+ # Budget events
310
+ def log_budget_created(
311
+ self,
312
+ budget_name: str,
313
+ limit: float,
314
+ period: str,
315
+ action: str,
316
+ actor: Optional[str] = None,
317
+ ) -> None:
318
+ """Log budget creation."""
319
+ self._log(AuditEvent(
320
+ event_type=AuditEventType.BUDGET_CREATED,
321
+ actor=actor,
322
+ resource=budget_name,
323
+ details={
324
+ "limit": limit,
325
+ "period": period,
326
+ "action": action,
327
+ },
328
+ ))
329
+
330
+ def log_budget_modified(
331
+ self,
332
+ budget_name: str,
333
+ old_values: Dict[str, Any],
334
+ new_values: Dict[str, Any],
335
+ actor: Optional[str] = None,
336
+ ) -> None:
337
+ """Log budget modification."""
338
+ self._log(AuditEvent(
339
+ event_type=AuditEventType.BUDGET_MODIFIED,
340
+ actor=actor,
341
+ resource=budget_name,
342
+ details={
343
+ "old_values": old_values,
344
+ "new_values": new_values,
345
+ },
346
+ ))
347
+
348
+ def log_budget_deleted(
349
+ self,
350
+ budget_name: str,
351
+ actor: Optional[str] = None,
352
+ ) -> None:
353
+ """Log budget deletion."""
354
+ self._log(AuditEvent(
355
+ event_type=AuditEventType.BUDGET_DELETED,
356
+ actor=actor,
357
+ resource=budget_name,
358
+ ))
359
+
360
+ def log_budget_warning(
361
+ self,
362
+ budget_name: str,
363
+ current_spending: float,
364
+ limit: float,
365
+ utilization: float,
366
+ ) -> None:
367
+ """Log budget warning."""
368
+ self._log(AuditEvent(
369
+ event_type=AuditEventType.BUDGET_WARNING,
370
+ resource=budget_name,
371
+ details={
372
+ "current_spending": current_spending,
373
+ "limit": limit,
374
+ "utilization_percent": utilization * 100,
375
+ },
376
+ ))
377
+
378
+ def log_budget_exceeded(
379
+ self,
380
+ budget_name: str,
381
+ current_spending: float,
382
+ limit: float,
383
+ action_taken: str,
384
+ ) -> None:
385
+ """Log budget exceeded event."""
386
+ self._log(AuditEvent(
387
+ event_type=AuditEventType.BUDGET_EXCEEDED,
388
+ resource=budget_name,
389
+ details={
390
+ "current_spending": current_spending,
391
+ "limit": limit,
392
+ "action_taken": action_taken,
393
+ },
394
+ ))
395
+
396
+ def log_budget_reset(
397
+ self,
398
+ budget_name: str,
399
+ reason: str = "period_end",
400
+ actor: Optional[str] = None,
401
+ ) -> None:
402
+ """Log budget reset."""
403
+ self._log(AuditEvent(
404
+ event_type=AuditEventType.BUDGET_RESET,
405
+ actor=actor,
406
+ resource=budget_name,
407
+ details={"reason": reason},
408
+ ))
409
+
410
+ # Rate limit events
411
+ def log_rate_limit_exceeded(
412
+ self,
413
+ limit_name: str,
414
+ current: int,
415
+ limit: int,
416
+ retry_after: float,
417
+ ) -> None:
418
+ """Log rate limit exceeded event."""
419
+ self._log(AuditEvent(
420
+ event_type=AuditEventType.RATE_LIMIT_EXCEEDED,
421
+ resource=limit_name,
422
+ details={
423
+ "current": current,
424
+ "limit": limit,
425
+ "retry_after_seconds": retry_after,
426
+ },
427
+ ))
428
+
429
+ # Tracking events
430
+ def log_tracking_failure(
431
+ self,
432
+ error: str,
433
+ backend: str,
434
+ action_taken: str,
435
+ ) -> None:
436
+ """Log tracking failure."""
437
+ self._log(AuditEvent(
438
+ event_type=AuditEventType.TRACKING_FAILURE,
439
+ resource=backend,
440
+ details={
441
+ "error": error,
442
+ "action_taken": action_taken,
443
+ },
444
+ ))
445
+
446
+ def log_fallback_activated(
447
+ self,
448
+ original_backend: str,
449
+ fallback_backend: str,
450
+ reason: str,
451
+ ) -> None:
452
+ """Log fallback activation."""
453
+ self._log(AuditEvent(
454
+ event_type=AuditEventType.FALLBACK_ACTIVATED,
455
+ details={
456
+ "original_backend": original_backend,
457
+ "fallback_backend": fallback_backend,
458
+ "reason": reason,
459
+ },
460
+ ))
461
+
462
+ # Query methods
463
+ def query(
464
+ self,
465
+ event_type: Optional[AuditEventType] = None,
466
+ start_date: Optional[datetime] = None,
467
+ end_date: Optional[datetime] = None,
468
+ resource: Optional[str] = None,
469
+ limit: int = 100,
470
+ ) -> List[AuditEvent]:
471
+ """Query audit events."""
472
+ return self._backend.query(event_type, start_date, end_date, resource, limit)
473
+
474
+ def get_budget_history(
475
+ self,
476
+ budget_name: str,
477
+ limit: int = 100,
478
+ ) -> List[AuditEvent]:
479
+ """Get audit history for a specific budget."""
480
+ return self._backend.query(resource=budget_name, limit=limit)
@@ -40,7 +40,7 @@ def get_backend(backend_url: str, **kwargs) -> Backend:
40
40
  return PostgresBackend(backend_url, **kwargs)
41
41
 
42
42
  if backend_url.startswith("redis://"):
43
- from llm_cost_guard.backends.redis import RedisBackend
43
+ from llm_cost_guard.backends.redis_backend import RedisBackend
44
44
 
45
45
  return RedisBackend(backend_url, **kwargs)
46
46