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.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +437 -10
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +11 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.0.dist-info/METADATA +309 -0
- {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.3.dist-info/METADATA +0 -505
- {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.3.dist-info → truthound_dashboard-1.5.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|