truthound-dashboard 1.4.3__py3-none-any.whl → 1.5.0__py3-none-any.whl

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 (205) hide show
  1. truthound_dashboard/api/alerts.py +75 -86
  2. truthound_dashboard/api/anomaly.py +7 -13
  3. truthound_dashboard/api/cross_alerts.py +38 -52
  4. truthound_dashboard/api/drift.py +49 -59
  5. truthound_dashboard/api/drift_monitor.py +234 -79
  6. truthound_dashboard/api/enterprise_sampling.py +498 -0
  7. truthound_dashboard/api/history.py +57 -5
  8. truthound_dashboard/api/lineage.py +3 -48
  9. truthound_dashboard/api/maintenance.py +104 -49
  10. truthound_dashboard/api/mask.py +1 -2
  11. truthound_dashboard/api/middleware.py +2 -1
  12. truthound_dashboard/api/model_monitoring.py +435 -311
  13. truthound_dashboard/api/notifications.py +227 -191
  14. truthound_dashboard/api/notifications_advanced.py +21 -20
  15. truthound_dashboard/api/observability.py +586 -0
  16. truthound_dashboard/api/plugins.py +2 -433
  17. truthound_dashboard/api/profile.py +199 -37
  18. truthound_dashboard/api/quality_reporter.py +701 -0
  19. truthound_dashboard/api/reports.py +7 -16
  20. truthound_dashboard/api/router.py +66 -0
  21. truthound_dashboard/api/rule_suggestions.py +5 -5
  22. truthound_dashboard/api/scan.py +17 -19
  23. truthound_dashboard/api/schedules.py +85 -50
  24. truthound_dashboard/api/schema_evolution.py +6 -6
  25. truthound_dashboard/api/schema_watcher.py +667 -0
  26. truthound_dashboard/api/sources.py +98 -27
  27. truthound_dashboard/api/tiering.py +1323 -0
  28. truthound_dashboard/api/triggers.py +14 -11
  29. truthound_dashboard/api/validations.py +12 -11
  30. truthound_dashboard/api/versioning.py +1 -6
  31. truthound_dashboard/core/__init__.py +129 -3
  32. truthound_dashboard/core/actions/__init__.py +62 -0
  33. truthound_dashboard/core/actions/custom.py +426 -0
  34. truthound_dashboard/core/actions/notifications.py +910 -0
  35. truthound_dashboard/core/actions/storage.py +472 -0
  36. truthound_dashboard/core/actions/webhook.py +281 -0
  37. truthound_dashboard/core/anomaly.py +262 -67
  38. truthound_dashboard/core/anomaly_explainer.py +4 -3
  39. truthound_dashboard/core/backends/__init__.py +67 -0
  40. truthound_dashboard/core/backends/base.py +299 -0
  41. truthound_dashboard/core/backends/errors.py +191 -0
  42. truthound_dashboard/core/backends/factory.py +423 -0
  43. truthound_dashboard/core/backends/mock_backend.py +451 -0
  44. truthound_dashboard/core/backends/truthound_backend.py +718 -0
  45. truthound_dashboard/core/checkpoint/__init__.py +87 -0
  46. truthound_dashboard/core/checkpoint/adapters.py +814 -0
  47. truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
  48. truthound_dashboard/core/checkpoint/runner.py +270 -0
  49. truthound_dashboard/core/connections.py +437 -10
  50. truthound_dashboard/core/converters/__init__.py +14 -0
  51. truthound_dashboard/core/converters/truthound.py +620 -0
  52. truthound_dashboard/core/cross_alerts.py +540 -320
  53. truthound_dashboard/core/datasource_factory.py +1672 -0
  54. truthound_dashboard/core/drift_monitor.py +216 -20
  55. truthound_dashboard/core/enterprise_sampling.py +1291 -0
  56. truthound_dashboard/core/interfaces/__init__.py +225 -0
  57. truthound_dashboard/core/interfaces/actions.py +652 -0
  58. truthound_dashboard/core/interfaces/base.py +247 -0
  59. truthound_dashboard/core/interfaces/checkpoint.py +676 -0
  60. truthound_dashboard/core/interfaces/protocols.py +664 -0
  61. truthound_dashboard/core/interfaces/reporters.py +650 -0
  62. truthound_dashboard/core/interfaces/routing.py +646 -0
  63. truthound_dashboard/core/interfaces/triggers.py +619 -0
  64. truthound_dashboard/core/lineage.py +407 -71
  65. truthound_dashboard/core/model_monitoring.py +431 -3
  66. truthound_dashboard/core/notifications/base.py +4 -0
  67. truthound_dashboard/core/notifications/channels.py +501 -1203
  68. truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
  69. truthound_dashboard/core/notifications/deduplication/service.py +131 -348
  70. truthound_dashboard/core/notifications/dispatcher.py +202 -11
  71. truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
  72. truthound_dashboard/core/notifications/escalation/engine.py +168 -358
  73. truthound_dashboard/core/notifications/routing/__init__.py +88 -128
  74. truthound_dashboard/core/notifications/routing/engine.py +90 -317
  75. truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
  76. truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
  77. truthound_dashboard/core/notifications/throttling/builder.py +117 -255
  78. truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
  79. truthound_dashboard/core/phase5/collaboration.py +1 -1
  80. truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
  81. truthound_dashboard/core/quality_reporter.py +1359 -0
  82. truthound_dashboard/core/report_history.py +0 -6
  83. truthound_dashboard/core/reporters/__init__.py +175 -14
  84. truthound_dashboard/core/reporters/adapters.py +943 -0
  85. truthound_dashboard/core/reporters/base.py +0 -3
  86. truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
  87. truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
  88. truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
  89. truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
  90. truthound_dashboard/core/reporters/compat.py +266 -0
  91. truthound_dashboard/core/reporters/csv_reporter.py +2 -35
  92. truthound_dashboard/core/reporters/factory.py +526 -0
  93. truthound_dashboard/core/reporters/interfaces.py +745 -0
  94. truthound_dashboard/core/reporters/registry.py +1 -10
  95. truthound_dashboard/core/scheduler.py +165 -0
  96. truthound_dashboard/core/schema_evolution.py +3 -3
  97. truthound_dashboard/core/schema_watcher.py +1528 -0
  98. truthound_dashboard/core/services.py +595 -76
  99. truthound_dashboard/core/store_manager.py +810 -0
  100. truthound_dashboard/core/streaming_anomaly.py +169 -4
  101. truthound_dashboard/core/tiering.py +1309 -0
  102. truthound_dashboard/core/triggers/evaluators.py +178 -8
  103. truthound_dashboard/core/truthound_adapter.py +2620 -197
  104. truthound_dashboard/core/unified_alerts.py +23 -20
  105. truthound_dashboard/db/__init__.py +8 -0
  106. truthound_dashboard/db/database.py +8 -2
  107. truthound_dashboard/db/models.py +944 -25
  108. truthound_dashboard/db/repository.py +2 -0
  109. truthound_dashboard/main.py +11 -0
  110. truthound_dashboard/schemas/__init__.py +177 -16
  111. truthound_dashboard/schemas/base.py +44 -23
  112. truthound_dashboard/schemas/collaboration.py +19 -6
  113. truthound_dashboard/schemas/cross_alerts.py +19 -3
  114. truthound_dashboard/schemas/drift.py +61 -55
  115. truthound_dashboard/schemas/drift_monitor.py +67 -23
  116. truthound_dashboard/schemas/enterprise_sampling.py +653 -0
  117. truthound_dashboard/schemas/lineage.py +0 -33
  118. truthound_dashboard/schemas/mask.py +10 -8
  119. truthound_dashboard/schemas/model_monitoring.py +89 -10
  120. truthound_dashboard/schemas/notifications_advanced.py +13 -0
  121. truthound_dashboard/schemas/observability.py +453 -0
  122. truthound_dashboard/schemas/plugins.py +0 -280
  123. truthound_dashboard/schemas/profile.py +154 -247
  124. truthound_dashboard/schemas/quality_reporter.py +403 -0
  125. truthound_dashboard/schemas/reports.py +2 -2
  126. truthound_dashboard/schemas/rule_suggestion.py +8 -1
  127. truthound_dashboard/schemas/scan.py +4 -24
  128. truthound_dashboard/schemas/schedule.py +11 -3
  129. truthound_dashboard/schemas/schema_watcher.py +727 -0
  130. truthound_dashboard/schemas/source.py +17 -2
  131. truthound_dashboard/schemas/tiering.py +822 -0
  132. truthound_dashboard/schemas/triggers.py +16 -0
  133. truthound_dashboard/schemas/unified_alerts.py +7 -0
  134. truthound_dashboard/schemas/validation.py +0 -13
  135. truthound_dashboard/schemas/validators/base.py +41 -21
  136. truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
  137. truthound_dashboard/schemas/validators/localization_validators.py +273 -0
  138. truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
  139. truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
  140. truthound_dashboard/schemas/validators/referential_validators.py +312 -0
  141. truthound_dashboard/schemas/validators/registry.py +93 -8
  142. truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
  143. truthound_dashboard/schemas/versioning.py +1 -6
  144. truthound_dashboard/static/index.html +2 -2
  145. truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
  146. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
  147. truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
  148. truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
  149. truthound_dashboard/core/plugins/hooks/manager.py +0 -403
  150. truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
  151. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
  152. truthound_dashboard/core/reporters/junit_reporter.py +0 -233
  153. truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
  154. truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
  155. truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
  156. truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
  157. truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
  158. truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
  159. truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
  160. truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
  161. truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
  162. truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
  163. truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
  164. truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
  165. truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
  166. truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
  167. truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
  168. truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
  169. truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
  170. truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
  171. truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
  172. truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
  173. truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
  174. truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
  175. truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
  176. truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
  177. truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
  178. truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
  179. truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
  180. truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
  181. truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
  182. truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
  183. truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
  184. truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
  185. truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
  186. truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
  187. truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
  188. truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
  189. truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
  190. truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
  191. truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
  192. truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
  193. truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
  194. truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
  195. truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
  196. truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
  197. truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
  198. truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
  199. truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
  200. truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
  201. truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
  202. truthound_dashboard-1.4.3.dist-info/METADATA +0 -505
  203. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
  204. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
  205. {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,586 @@
1
+ """Observability API endpoints.
2
+
3
+ This module provides endpoints for audit logging, metrics, and tracing
4
+ using truthound's observability module.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from datetime import datetime, timedelta
11
+ from typing import Any
12
+
13
+ from fastapi import APIRouter, HTTPException, Query
14
+
15
+ from truthound_dashboard.core.store_manager import get_store_manager
16
+ from truthound_dashboard.schemas.observability import (
17
+ AuditEventListResponse,
18
+ AuditEventResponse,
19
+ AuditEventTypeEnum,
20
+ AuditQueryRequest,
21
+ AuditStatsResponse,
22
+ AuditStatusEnum,
23
+ MetricsResponse,
24
+ MetricValue,
25
+ ObservabilityConfigRequest,
26
+ ObservabilityConfigResponse,
27
+ ObservabilityStatsResponse,
28
+ SpanListResponse,
29
+ SpanResponse,
30
+ StoreMetricsResponse,
31
+ TracingStatsResponse,
32
+ )
33
+
34
+ router = APIRouter()
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # Store observability config
38
+ _observability_config = ObservabilityConfigRequest()
39
+
40
+
41
+ @router.get(
42
+ "/config",
43
+ response_model=ObservabilityConfigResponse,
44
+ summary="Get observability config",
45
+ description="Get current observability configuration",
46
+ )
47
+ async def get_observability_config() -> ObservabilityConfigResponse:
48
+ """Get current observability configuration.
49
+
50
+ Returns:
51
+ Current observability configuration.
52
+ """
53
+ return ObservabilityConfigResponse(**_observability_config.model_dump())
54
+
55
+
56
+ @router.put(
57
+ "/config",
58
+ response_model=ObservabilityConfigResponse,
59
+ summary="Update observability config",
60
+ description="Update observability configuration",
61
+ )
62
+ async def update_observability_config(
63
+ config: ObservabilityConfigRequest,
64
+ ) -> ObservabilityConfigResponse:
65
+ """Update observability configuration.
66
+
67
+ Args:
68
+ config: New observability configuration.
69
+
70
+ Returns:
71
+ Updated configuration.
72
+ """
73
+ global _observability_config
74
+ _observability_config = config
75
+
76
+ logger.info(
77
+ f"Observability config updated: "
78
+ f"audit={config.enable_audit}, "
79
+ f"metrics={config.enable_metrics}, "
80
+ f"tracing={config.enable_tracing}"
81
+ )
82
+
83
+ return ObservabilityConfigResponse(**config.model_dump())
84
+
85
+
86
+ @router.get(
87
+ "/stats",
88
+ response_model=ObservabilityStatsResponse,
89
+ summary="Get observability stats",
90
+ description="Get combined statistics from audit, metrics, and tracing",
91
+ )
92
+ async def get_observability_stats() -> ObservabilityStatsResponse:
93
+ """Get combined observability statistics.
94
+
95
+ Returns:
96
+ Combined statistics from all observability pillars.
97
+ """
98
+ store_manager = get_store_manager()
99
+ if not store_manager._initialized:
100
+ store_manager.initialize()
101
+
102
+ # Audit stats
103
+ audit_stats = AuditStatsResponse(
104
+ total_events=0,
105
+ events_today=0,
106
+ events_this_week=0,
107
+ by_event_type={},
108
+ by_status={},
109
+ error_rate=0.0,
110
+ avg_duration_ms=None,
111
+ )
112
+
113
+ try:
114
+ events = store_manager.get_audit_events(limit=1000)
115
+ if events:
116
+ today = datetime.utcnow().date()
117
+ week_ago = datetime.utcnow() - timedelta(days=7)
118
+
119
+ events_today = sum(1 for e in events if e.timestamp.date() == today)
120
+ events_week = sum(1 for e in events if e.timestamp >= week_ago)
121
+
122
+ by_type: dict[str, int] = {}
123
+ by_status: dict[str, int] = {}
124
+ error_count = 0
125
+ total_duration = 0.0
126
+ duration_count = 0
127
+
128
+ for e in events:
129
+ by_type[e.event_type.value] = by_type.get(e.event_type.value, 0) + 1
130
+ by_status[e.status.value] = by_status.get(e.status.value, 0) + 1
131
+ if e.status.value in ("failure", "error"):
132
+ error_count += 1
133
+ if e.duration_ms:
134
+ total_duration += e.duration_ms
135
+ duration_count += 1
136
+
137
+ audit_stats = AuditStatsResponse(
138
+ total_events=len(events),
139
+ events_today=events_today,
140
+ events_this_week=events_week,
141
+ by_event_type=by_type,
142
+ by_status=by_status,
143
+ error_rate=error_count / len(events) if events else 0.0,
144
+ avg_duration_ms=total_duration / duration_count if duration_count else None,
145
+ )
146
+ except Exception as e:
147
+ logger.warning(f"Failed to get audit stats: {e}")
148
+
149
+ # Store metrics
150
+ store_metrics = StoreMetricsResponse(
151
+ operations_total=0,
152
+ operations_by_type={},
153
+ bytes_read_total=0,
154
+ bytes_written_total=0,
155
+ active_connections=0,
156
+ cache_hits=0,
157
+ cache_misses=0,
158
+ cache_hit_rate=0.0,
159
+ errors_total=0,
160
+ errors_by_type={},
161
+ avg_operation_duration_ms=None,
162
+ )
163
+
164
+ try:
165
+ metrics = store_manager.get_store_metrics()
166
+ if metrics:
167
+ store_metrics = StoreMetricsResponse(**metrics)
168
+ except Exception as e:
169
+ logger.warning(f"Failed to get store metrics: {e}")
170
+
171
+ # Tracing stats (if enabled)
172
+ tracing_stats = None
173
+ if _observability_config.enable_tracing:
174
+ tracing_stats = TracingStatsResponse(
175
+ enabled=True,
176
+ total_traces=0,
177
+ total_spans=0,
178
+ avg_trace_duration_ms=None,
179
+ traces_today=0,
180
+ error_rate=0.0,
181
+ by_service={},
182
+ )
183
+
184
+ return ObservabilityStatsResponse(
185
+ audit=audit_stats,
186
+ store_metrics=store_metrics,
187
+ tracing=tracing_stats,
188
+ last_updated=datetime.utcnow(),
189
+ )
190
+
191
+
192
+ # =============================================================================
193
+ # Audit Endpoints
194
+ # =============================================================================
195
+
196
+
197
+ @router.get(
198
+ "/audit/events",
199
+ response_model=AuditEventListResponse,
200
+ summary="List audit events",
201
+ description="Query audit events with filters",
202
+ )
203
+ async def list_audit_events(
204
+ event_type: AuditEventTypeEnum | None = Query(None, description="Filter by event type"),
205
+ status: AuditStatusEnum | None = Query(None, description="Filter by status"),
206
+ start_time: datetime | None = Query(None, description="Filter after this time"),
207
+ end_time: datetime | None = Query(None, description="Filter before this time"),
208
+ item_id: str | None = Query(None, description="Filter by item ID"),
209
+ limit: int = Query(100, ge=1, le=1000, description="Maximum events"),
210
+ offset: int = Query(0, ge=0, description="Offset for pagination"),
211
+ ) -> AuditEventListResponse:
212
+ """List audit events with optional filters.
213
+
214
+ Args:
215
+ event_type: Filter by event type.
216
+ status: Filter by status.
217
+ start_time: Filter events after this time.
218
+ end_time: Filter events before this time.
219
+ item_id: Filter by item ID.
220
+ limit: Maximum events to return.
221
+ offset: Offset for pagination.
222
+
223
+ Returns:
224
+ List of audit events.
225
+ """
226
+ store_manager = get_store_manager()
227
+ if not store_manager._initialized:
228
+ store_manager.initialize()
229
+
230
+ try:
231
+ # Convert enum to truthound enum if provided
232
+ th_event_type = None
233
+ if event_type:
234
+ from truthound.stores.observability.audit import AuditEventType
235
+ th_event_type = AuditEventType(event_type.value)
236
+
237
+ events = store_manager.get_audit_events(
238
+ event_type=th_event_type,
239
+ start_time=start_time,
240
+ end_time=end_time,
241
+ limit=limit + offset, # Get extra for offset
242
+ )
243
+
244
+ # Apply offset and additional filters
245
+ filtered_events = []
246
+ for e in events[offset:]:
247
+ if item_id and e.item_id != item_id:
248
+ continue
249
+ if status and e.status.value != status.value:
250
+ continue
251
+
252
+ filtered_events.append(
253
+ AuditEventResponse(
254
+ event_id=e.event_id,
255
+ event_type=AuditEventTypeEnum(e.event_type.value),
256
+ timestamp=e.timestamp,
257
+ status=AuditStatusEnum(e.status.value),
258
+ store_type=e.store_type,
259
+ store_id=e.store_id,
260
+ item_id=e.item_id,
261
+ user_id=e.user_id,
262
+ session_id=e.session_id,
263
+ duration_ms=e.duration_ms,
264
+ metadata=e.metadata,
265
+ error_message=e.error_message,
266
+ ip_address=e.ip_address,
267
+ user_agent=e.user_agent,
268
+ )
269
+ )
270
+
271
+ if len(filtered_events) >= limit:
272
+ break
273
+
274
+ return AuditEventListResponse(
275
+ items=filtered_events,
276
+ total=len(events),
277
+ page=offset // limit + 1 if limit else 1,
278
+ page_size=limit,
279
+ )
280
+
281
+ except Exception as e:
282
+ logger.error(f"Failed to list audit events: {e}")
283
+ return AuditEventListResponse(
284
+ items=[],
285
+ total=0,
286
+ page=1,
287
+ page_size=limit,
288
+ )
289
+
290
+
291
+ @router.get(
292
+ "/audit/stats",
293
+ response_model=AuditStatsResponse,
294
+ summary="Get audit stats",
295
+ description="Get audit event statistics",
296
+ )
297
+ async def get_audit_stats() -> AuditStatsResponse:
298
+ """Get audit event statistics.
299
+
300
+ Returns:
301
+ Audit statistics.
302
+ """
303
+ store_manager = get_store_manager()
304
+ if not store_manager._initialized:
305
+ store_manager.initialize()
306
+
307
+ try:
308
+ events = store_manager.get_audit_events(limit=10000)
309
+
310
+ if not events:
311
+ return AuditStatsResponse(
312
+ total_events=0,
313
+ events_today=0,
314
+ events_this_week=0,
315
+ by_event_type={},
316
+ by_status={},
317
+ error_rate=0.0,
318
+ avg_duration_ms=None,
319
+ )
320
+
321
+ today = datetime.utcnow().date()
322
+ week_ago = datetime.utcnow() - timedelta(days=7)
323
+
324
+ events_today = sum(1 for e in events if e.timestamp.date() == today)
325
+ events_week = sum(1 for e in events if e.timestamp >= week_ago)
326
+
327
+ by_type: dict[str, int] = {}
328
+ by_status: dict[str, int] = {}
329
+ error_count = 0
330
+ total_duration = 0.0
331
+ duration_count = 0
332
+
333
+ for e in events:
334
+ by_type[e.event_type.value] = by_type.get(e.event_type.value, 0) + 1
335
+ by_status[e.status.value] = by_status.get(e.status.value, 0) + 1
336
+ if e.status.value in ("failure", "error"):
337
+ error_count += 1
338
+ if e.duration_ms:
339
+ total_duration += e.duration_ms
340
+ duration_count += 1
341
+
342
+ return AuditStatsResponse(
343
+ total_events=len(events),
344
+ events_today=events_today,
345
+ events_this_week=events_week,
346
+ by_event_type=by_type,
347
+ by_status=by_status,
348
+ error_rate=error_count / len(events) if events else 0.0,
349
+ avg_duration_ms=total_duration / duration_count if duration_count else None,
350
+ )
351
+
352
+ except Exception as e:
353
+ logger.error(f"Failed to get audit stats: {e}")
354
+ return AuditStatsResponse(
355
+ total_events=0,
356
+ events_today=0,
357
+ events_this_week=0,
358
+ by_event_type={},
359
+ by_status={},
360
+ error_rate=0.0,
361
+ avg_duration_ms=None,
362
+ )
363
+
364
+
365
+ # =============================================================================
366
+ # Metrics Endpoints
367
+ # =============================================================================
368
+
369
+
370
+ @router.get(
371
+ "/metrics",
372
+ response_model=MetricsResponse,
373
+ summary="Get all metrics",
374
+ description="Get all current metrics",
375
+ )
376
+ async def get_metrics() -> MetricsResponse:
377
+ """Get all current metrics.
378
+
379
+ Returns:
380
+ All metrics organized by type.
381
+ """
382
+ store_manager = get_store_manager()
383
+ if not store_manager._initialized:
384
+ store_manager.initialize()
385
+
386
+ try:
387
+ raw_metrics = store_manager.get_store_metrics()
388
+
389
+ # Convert to structured response
390
+ counters = []
391
+ gauges = []
392
+
393
+ if raw_metrics:
394
+ # Operations are counters
395
+ if "operations_total" in raw_metrics:
396
+ counters.append(MetricValue(
397
+ name="store_operations_total",
398
+ value=float(raw_metrics["operations_total"]),
399
+ labels={"store": "dashboard"},
400
+ ))
401
+
402
+ if "bytes_read_total" in raw_metrics:
403
+ counters.append(MetricValue(
404
+ name="store_bytes_read_total",
405
+ value=float(raw_metrics["bytes_read_total"]),
406
+ labels={"store": "dashboard"},
407
+ ))
408
+
409
+ if "bytes_written_total" in raw_metrics:
410
+ counters.append(MetricValue(
411
+ name="store_bytes_written_total",
412
+ value=float(raw_metrics["bytes_written_total"]),
413
+ labels={"store": "dashboard"},
414
+ ))
415
+
416
+ if "cache_hits" in raw_metrics:
417
+ counters.append(MetricValue(
418
+ name="store_cache_hits_total",
419
+ value=float(raw_metrics["cache_hits"]),
420
+ labels={"store": "dashboard"},
421
+ ))
422
+
423
+ if "cache_misses" in raw_metrics:
424
+ counters.append(MetricValue(
425
+ name="store_cache_misses_total",
426
+ value=float(raw_metrics["cache_misses"]),
427
+ labels={"store": "dashboard"},
428
+ ))
429
+
430
+ if "errors_total" in raw_metrics:
431
+ counters.append(MetricValue(
432
+ name="store_errors_total",
433
+ value=float(raw_metrics["errors_total"]),
434
+ labels={"store": "dashboard"},
435
+ ))
436
+
437
+ # Active connections is a gauge
438
+ if "active_connections" in raw_metrics:
439
+ gauges.append(MetricValue(
440
+ name="store_connections_active",
441
+ value=float(raw_metrics["active_connections"]),
442
+ labels={"store": "dashboard"},
443
+ ))
444
+
445
+ # Cache hit rate is a gauge
446
+ if "cache_hit_rate" in raw_metrics:
447
+ gauges.append(MetricValue(
448
+ name="store_cache_hit_rate",
449
+ value=float(raw_metrics["cache_hit_rate"]),
450
+ labels={"store": "dashboard"},
451
+ ))
452
+
453
+ return MetricsResponse(
454
+ counters=counters,
455
+ gauges=gauges,
456
+ histograms=[],
457
+ summaries=[],
458
+ timestamp=datetime.utcnow(),
459
+ )
460
+
461
+ except Exception as e:
462
+ logger.error(f"Failed to get metrics: {e}")
463
+ return MetricsResponse(
464
+ counters=[],
465
+ gauges=[],
466
+ histograms=[],
467
+ summaries=[],
468
+ timestamp=datetime.utcnow(),
469
+ )
470
+
471
+
472
+ @router.get(
473
+ "/metrics/store",
474
+ response_model=StoreMetricsResponse,
475
+ summary="Get store metrics",
476
+ description="Get store-specific metrics",
477
+ )
478
+ async def get_store_metrics() -> StoreMetricsResponse:
479
+ """Get store-specific metrics.
480
+
481
+ Returns:
482
+ Store metrics.
483
+ """
484
+ store_manager = get_store_manager()
485
+ if not store_manager._initialized:
486
+ store_manager.initialize()
487
+
488
+ try:
489
+ metrics = store_manager.get_store_metrics()
490
+
491
+ if not metrics:
492
+ return StoreMetricsResponse()
493
+
494
+ return StoreMetricsResponse(
495
+ operations_total=metrics.get("operations_total", 0),
496
+ operations_by_type=metrics.get("operations_by_type", {}),
497
+ bytes_read_total=metrics.get("bytes_read_total", 0),
498
+ bytes_written_total=metrics.get("bytes_written_total", 0),
499
+ active_connections=metrics.get("active_connections", 0),
500
+ cache_hits=metrics.get("cache_hits", 0),
501
+ cache_misses=metrics.get("cache_misses", 0),
502
+ cache_hit_rate=metrics.get("cache_hit_rate", 0.0),
503
+ errors_total=metrics.get("errors_total", 0),
504
+ errors_by_type=metrics.get("errors_by_type", {}),
505
+ avg_operation_duration_ms=metrics.get("avg_operation_duration_ms"),
506
+ )
507
+
508
+ except Exception as e:
509
+ logger.error(f"Failed to get store metrics: {e}")
510
+ return StoreMetricsResponse()
511
+
512
+
513
+ # =============================================================================
514
+ # Tracing Endpoints
515
+ # =============================================================================
516
+
517
+
518
+ @router.get(
519
+ "/tracing/stats",
520
+ response_model=TracingStatsResponse,
521
+ summary="Get tracing stats",
522
+ description="Get distributed tracing statistics",
523
+ )
524
+ async def get_tracing_stats() -> TracingStatsResponse:
525
+ """Get distributed tracing statistics.
526
+
527
+ Returns:
528
+ Tracing statistics.
529
+ """
530
+ if not _observability_config.enable_tracing:
531
+ return TracingStatsResponse(
532
+ enabled=False,
533
+ total_traces=0,
534
+ total_spans=0,
535
+ avg_trace_duration_ms=None,
536
+ traces_today=0,
537
+ error_rate=0.0,
538
+ by_service={},
539
+ )
540
+
541
+ # In a full implementation, this would query the tracing backend
542
+ return TracingStatsResponse(
543
+ enabled=True,
544
+ total_traces=0,
545
+ total_spans=0,
546
+ avg_trace_duration_ms=None,
547
+ traces_today=0,
548
+ error_rate=0.0,
549
+ by_service={},
550
+ )
551
+
552
+
553
+ @router.get(
554
+ "/tracing/spans",
555
+ response_model=SpanListResponse,
556
+ summary="List spans",
557
+ description="List recent spans (requires tracing to be enabled)",
558
+ )
559
+ async def list_spans(
560
+ limit: int = Query(100, ge=1, le=1000, description="Maximum spans"),
561
+ offset: int = Query(0, ge=0, description="Offset for pagination"),
562
+ ) -> SpanListResponse:
563
+ """List recent spans.
564
+
565
+ Args:
566
+ limit: Maximum spans to return.
567
+ offset: Offset for pagination.
568
+
569
+ Returns:
570
+ List of spans.
571
+ """
572
+ if not _observability_config.enable_tracing:
573
+ return SpanListResponse(
574
+ items=[],
575
+ total=0,
576
+ page=1,
577
+ page_size=limit,
578
+ )
579
+
580
+ # In a full implementation, this would query the tracing backend
581
+ return SpanListResponse(
582
+ items=[],
583
+ total=0,
584
+ page=offset // limit + 1 if limit else 1,
585
+ page_size=limit,
586
+ )