agentops-accelerator 0.3.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.
- agentops/__init__.py +10 -0
- agentops/__main__.py +6 -0
- agentops/agent/__init__.py +12 -0
- agentops/agent/_legacy_ids.py +92 -0
- agentops/agent/analyzer.py +207 -0
- agentops/agent/checks/__init__.py +1 -0
- agentops/agent/checks/catalog.py +880 -0
- agentops/agent/checks/errors.py +279 -0
- agentops/agent/checks/foundry_config.py +75 -0
- agentops/agent/checks/latency.py +84 -0
- agentops/agent/checks/opex.py +157 -0
- agentops/agent/checks/opex_workspace.py +874 -0
- agentops/agent/checks/posture.py +36 -0
- agentops/agent/checks/posture_rules/__init__.py +53 -0
- agentops/agent/checks/posture_rules/content_filter.py +59 -0
- agentops/agent/checks/posture_rules/diagnostics.py +74 -0
- agentops/agent/checks/posture_rules/local_auth.py +55 -0
- agentops/agent/checks/posture_rules/managed_identity.py +59 -0
- agentops/agent/checks/posture_rules/network.py +68 -0
- agentops/agent/checks/regression.py +78 -0
- agentops/agent/checks/release_readiness.py +182 -0
- agentops/agent/checks/safety.py +247 -0
- agentops/agent/checks/spec_conformance.py +375 -0
- agentops/agent/cockpit.py +5159 -0
- agentops/agent/config.py +240 -0
- agentops/agent/findings.py +113 -0
- agentops/agent/history.py +142 -0
- agentops/agent/knowledge/__init__.py +182 -0
- agentops/agent/knowledge/waf-checklist.csv +39 -0
- agentops/agent/llm_assist/__init__.py +16 -0
- agentops/agent/llm_assist/_base.py +124 -0
- agentops/agent/llm_assist/_bundle_rule.py +154 -0
- agentops/agent/llm_assist/_client.py +347 -0
- agentops/agent/llm_assist/_dataset_rules.py +191 -0
- agentops/agent/llm_assist/_engine.py +106 -0
- agentops/agent/llm_assist/_prompt_rules.py +291 -0
- agentops/agent/llm_assist/_spec_rules.py +235 -0
- agentops/agent/production_telemetry.py +430 -0
- agentops/agent/report.py +207 -0
- agentops/agent/server/__init__.py +1 -0
- agentops/agent/server/app.py +84 -0
- agentops/agent/server/auth.py +94 -0
- agentops/agent/server/chat.py +44 -0
- agentops/agent/server/protocol.py +72 -0
- agentops/agent/sources/__init__.py +1 -0
- agentops/agent/sources/azure_monitor.py +523 -0
- agentops/agent/sources/azure_resources.py +602 -0
- agentops/agent/sources/foundry_control.py +174 -0
- agentops/agent/sources/results_history.py +494 -0
- agentops/agent/sources/spec_detectors/__init__.py +42 -0
- agentops/agent/sources/spec_detectors/_base.py +58 -0
- agentops/agent/sources/spec_detectors/agents_md.py +75 -0
- agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
- agentops/agent/time_range.py +117 -0
- agentops/cli/__init__.py +1 -0
- agentops/cli/app.py +4823 -0
- agentops/core/__init__.py +1 -0
- agentops/core/agentops_config.py +592 -0
- agentops/core/config_loader.py +22 -0
- agentops/core/evaluators.py +480 -0
- agentops/core/release_evidence.py +56 -0
- agentops/core/results.py +117 -0
- agentops/mcp/__init__.py +10 -0
- agentops/mcp/server.py +232 -0
- agentops/pipeline/__init__.py +8 -0
- agentops/pipeline/cloud_results.py +189 -0
- agentops/pipeline/cloud_runner.py +901 -0
- agentops/pipeline/comparison.py +108 -0
- agentops/pipeline/diagnostics.py +51 -0
- agentops/pipeline/invocations.py +535 -0
- agentops/pipeline/official_eval.py +414 -0
- agentops/pipeline/orchestrator.py +775 -0
- agentops/pipeline/prompt_deploy.py +377 -0
- agentops/pipeline/publisher.py +121 -0
- agentops/pipeline/reporter.py +202 -0
- agentops/pipeline/runtime.py +409 -0
- agentops/pipeline/thresholds.py +84 -0
- agentops/services/__init__.py +1 -0
- agentops/services/cicd.py +720 -0
- agentops/services/eval_analysis.py +848 -0
- agentops/services/evidence_pack.py +757 -0
- agentops/services/initializer.py +86 -0
- agentops/services/preflight.py +470 -0
- agentops/services/setup_wizard.py +709 -0
- agentops/services/skills.py +643 -0
- agentops/services/trace_promotion.py +300 -0
- agentops/services/workflow_analysis.py +1129 -0
- agentops/templates/.gitignore +15 -0
- agentops/templates/__init__.py +1 -0
- agentops/templates/agent-server/Dockerfile +23 -0
- agentops/templates/agent-server/README.md +61 -0
- agentops/templates/agent-server/main.bicep +94 -0
- agentops/templates/agent.yaml +87 -0
- agentops/templates/agentops.yaml +58 -0
- agentops/templates/foundry.svg +71 -0
- agentops/templates/icon.png +0 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
- agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
- agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
- agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
- agentops/templates/project.gitignore +36 -0
- agentops/templates/sample-traces.jsonl +3 -0
- agentops/templates/skills/agentops-agent/SKILL.md +137 -0
- agentops/templates/skills/agentops-config/SKILL.md +113 -0
- agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
- agentops/templates/skills/agentops-eval/SKILL.md +189 -0
- agentops/templates/skills/agentops-report/SKILL.md +71 -0
- agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
- agentops/templates/smoke.jsonl +3 -0
- agentops/templates/waf-checklist.README.md +84 -0
- agentops/templates/waf-checklist.csv +22 -0
- agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
- agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
- agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
- agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
- agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
- agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
- agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
- agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
- agentops/templates/workflows/agentops-pr.yml +148 -0
- agentops/templates/workflows/agentops-watchdog.yml +122 -0
- agentops/utils/__init__.py +1 -0
- agentops/utils/azd_env.py +435 -0
- agentops/utils/azure_endpoints.py +62 -0
- agentops/utils/colors.py +47 -0
- agentops/utils/dotenv_loader.py +105 -0
- agentops/utils/foundry_discovery.py +229 -0
- agentops/utils/logging.py +59 -0
- agentops/utils/telemetry.py +554 -0
- agentops/utils/yaml.py +36 -0
- agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
- agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
- agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
- agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
- agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
- agentops_accelerator-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"""Azure Monitor / Application Insights source.
|
|
2
|
+
|
|
3
|
+
Lazy-imports ``azure.monitor.query`` at call time so the base CLI does
|
|
4
|
+
not require the SDK. When the source is not configured or the SDK is
|
|
5
|
+
not installed, returns an empty payload with a diagnostic note.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
from urllib import error, request
|
|
17
|
+
|
|
18
|
+
from agentops.agent.config import AzureMonitorSourceConfig
|
|
19
|
+
|
|
20
|
+
log = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class AzureMonitorPayload:
|
|
25
|
+
request_count: int = 0
|
|
26
|
+
error_count: int = 0
|
|
27
|
+
p95_duration_seconds: Optional[float] = None
|
|
28
|
+
avg_duration_seconds: Optional[float] = None
|
|
29
|
+
error_rate: Optional[float] = None
|
|
30
|
+
safety_violations: List[Dict[str, Any]] = field(default_factory=list)
|
|
31
|
+
# AI.132 — token-usage telemetry (gen_ai.usage.*). When the agent
|
|
32
|
+
# runtime instruments OpenTelemetry token attributes these counts
|
|
33
|
+
# are non-zero; zero across a populated request_count window means
|
|
34
|
+
# the runtime is not emitting token telemetry.
|
|
35
|
+
input_token_count: Optional[int] = None
|
|
36
|
+
output_token_count: Optional[int] = None
|
|
37
|
+
# AI.154 — count of rate-limit responses (HTTP 429 in dependency
|
|
38
|
+
# telemetry). High values indicate quota / TPM / RPM pressure.
|
|
39
|
+
rate_limit_429_count: int = 0
|
|
40
|
+
diagnostics: Dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_REQUESTS_KQL = """
|
|
44
|
+
union isfuzzy=true requests, dependencies
|
|
45
|
+
| where timestamp > ago({lookback_days}d)
|
|
46
|
+
| summarize
|
|
47
|
+
request_count = count(),
|
|
48
|
+
error_count = countif(success == false),
|
|
49
|
+
avg_duration_ms = avg(duration),
|
|
50
|
+
p95_duration_ms = percentile(duration, 95)
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Detects OpenAI / Foundry content-filter triggers in App Insights /
|
|
55
|
+
# Log Analytics. We intentionally stay conservative: the only attribute
|
|
56
|
+
# guaranteed by the OTel Gen AI semantic conventions is
|
|
57
|
+
# ``gen_ai.response.finish_reasons`` (plural), which contains
|
|
58
|
+
# ``content_filter`` when the model refused to complete a response. Any
|
|
59
|
+
# other category/severity breakdown is vendor-specific and not relied
|
|
60
|
+
# upon here.
|
|
61
|
+
_SAFETY_KQL = """
|
|
62
|
+
union isfuzzy=true requests, dependencies, traces, AppTraces, AppDependencies, AppRequests
|
|
63
|
+
| where timestamp > ago({lookback_days}d)
|
|
64
|
+
| extend cd = tostring(customDimensions)
|
|
65
|
+
| where cd has "content_filter"
|
|
66
|
+
| summarize hits = count()
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# AI.132 — token usage probe. Sums input / output token counts emitted
|
|
71
|
+
# by OpenTelemetry semantic conventions (``gen_ai.usage.input_tokens``,
|
|
72
|
+
# ``gen_ai.usage.output_tokens``). Falls back to the legacy
|
|
73
|
+
# ``llm.usage.*`` keys some SDKs still emit. ``toint(coalesce(...))``
|
|
74
|
+
# tolerates missing keys per row.
|
|
75
|
+
_TOKEN_USAGE_KQL = """
|
|
76
|
+
dependencies
|
|
77
|
+
| where timestamp > ago({lookback_days}d)
|
|
78
|
+
| extend input_t = toint(coalesce(
|
|
79
|
+
customDimensions["gen_ai.usage.input_tokens"],
|
|
80
|
+
customDimensions["llm.usage.input_tokens"]
|
|
81
|
+
))
|
|
82
|
+
| extend output_t = toint(coalesce(
|
|
83
|
+
customDimensions["gen_ai.usage.output_tokens"],
|
|
84
|
+
customDimensions["llm.usage.output_tokens"]
|
|
85
|
+
))
|
|
86
|
+
| where isnotnull(input_t) or isnotnull(output_t)
|
|
87
|
+
| summarize input_tokens = sum(input_t), output_tokens = sum(output_t)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# AI.154 — rate-limit pressure. Counts dependency calls whose HTTP
|
|
92
|
+
# resultCode is 429 (Too Many Requests). Azure OpenAI surfaces TPM/RPM
|
|
93
|
+
# exhaustion this way before degrading further.
|
|
94
|
+
_RATE_LIMIT_KQL = """
|
|
95
|
+
dependencies
|
|
96
|
+
| where timestamp > ago({lookback_days}d)
|
|
97
|
+
| where toint(resultCode) == 429
|
|
98
|
+
| summarize hits = count()
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def collect_azure_monitor(
|
|
103
|
+
config: AzureMonitorSourceConfig,
|
|
104
|
+
lookback_days: int,
|
|
105
|
+
) -> AzureMonitorPayload:
|
|
106
|
+
"""Run KQL queries against Application Insights for the lookback window."""
|
|
107
|
+
diagnostics: Dict[str, Any] = {"enabled": config.enabled}
|
|
108
|
+
|
|
109
|
+
if not config.enabled:
|
|
110
|
+
diagnostics["status"] = "disabled"
|
|
111
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
112
|
+
|
|
113
|
+
if not config.app_insights_resource_id and not config.log_analytics_workspace_id:
|
|
114
|
+
application_id, source, reason = _resolve_application_id()
|
|
115
|
+
if application_id:
|
|
116
|
+
diagnostics["target"] = application_id
|
|
117
|
+
diagnostics["target_kind"] = "application_id"
|
|
118
|
+
diagnostics["target_source"] = source
|
|
119
|
+
return _collect_application_insights_by_app_id(
|
|
120
|
+
application_id,
|
|
121
|
+
lookback_days,
|
|
122
|
+
diagnostics,
|
|
123
|
+
)
|
|
124
|
+
diagnostics["status"] = "skipped"
|
|
125
|
+
diagnostics["reason"] = (
|
|
126
|
+
"neither app_insights_resource_id nor log_analytics_workspace_id "
|
|
127
|
+
"is configured, and no App Insights ApplicationId could be "
|
|
128
|
+
"discovered from the connection string or Foundry project"
|
|
129
|
+
)
|
|
130
|
+
if reason:
|
|
131
|
+
diagnostics["discovery_reason"] = reason
|
|
132
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
from azure.identity import DefaultAzureCredential
|
|
136
|
+
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
|
|
137
|
+
except ImportError as exc:
|
|
138
|
+
diagnostics["status"] = "skipped"
|
|
139
|
+
diagnostics["reason"] = (
|
|
140
|
+
"azure-monitor-query / azure-identity not installed "
|
|
141
|
+
"(install agentops-accelerator[agent])"
|
|
142
|
+
)
|
|
143
|
+
log.info("azure-monitor-query unavailable: %s", exc)
|
|
144
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
145
|
+
|
|
146
|
+
workspace_or_resource = (
|
|
147
|
+
config.log_analytics_workspace_id or config.app_insights_resource_id
|
|
148
|
+
)
|
|
149
|
+
diagnostics["target"] = workspace_or_resource
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
credential = DefaultAzureCredential(
|
|
153
|
+
exclude_developer_cli_credential=True,
|
|
154
|
+
process_timeout=30,
|
|
155
|
+
)
|
|
156
|
+
client = LogsQueryClient(credential)
|
|
157
|
+
kql = _REQUESTS_KQL.format(lookback_days=int(lookback_days))
|
|
158
|
+
if config.log_analytics_workspace_id:
|
|
159
|
+
response = client.query_workspace(
|
|
160
|
+
workspace_id=config.log_analytics_workspace_id,
|
|
161
|
+
query=kql,
|
|
162
|
+
timespan=None,
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
# query_resource is available on newer SDKs.
|
|
166
|
+
query_resource = getattr(client, "query_resource", None)
|
|
167
|
+
if query_resource is None:
|
|
168
|
+
diagnostics["status"] = "skipped"
|
|
169
|
+
diagnostics["reason"] = (
|
|
170
|
+
"Installed azure-monitor-query does not support "
|
|
171
|
+
"query_resource; upgrade to >=1.3.0 or use "
|
|
172
|
+
"log_analytics_workspace_id."
|
|
173
|
+
)
|
|
174
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
175
|
+
response = query_resource(
|
|
176
|
+
resource_id=config.app_insights_resource_id,
|
|
177
|
+
query=kql,
|
|
178
|
+
timespan=None,
|
|
179
|
+
)
|
|
180
|
+
except Exception as exc: # pragma: no cover - network / auth errors
|
|
181
|
+
diagnostics["status"] = "error"
|
|
182
|
+
diagnostics["reason"] = str(exc)
|
|
183
|
+
log.warning("Azure Monitor query failed: %s", exc)
|
|
184
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
185
|
+
|
|
186
|
+
if getattr(response, "status", None) == LogsQueryStatus.FAILURE:
|
|
187
|
+
diagnostics["status"] = "error"
|
|
188
|
+
diagnostics["reason"] = "query failed"
|
|
189
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
190
|
+
|
|
191
|
+
payload = AzureMonitorPayload(diagnostics=diagnostics)
|
|
192
|
+
diagnostics["status"] = "ok"
|
|
193
|
+
|
|
194
|
+
tables = getattr(response, "tables", []) or []
|
|
195
|
+
if tables:
|
|
196
|
+
rows = list(tables[0].rows)
|
|
197
|
+
if rows:
|
|
198
|
+
row = rows[0]
|
|
199
|
+
columns = [c.name if hasattr(c, "name") else str(c) for c in tables[0].columns]
|
|
200
|
+
data = dict(zip(columns, row))
|
|
201
|
+
payload.request_count = int(data.get("request_count", 0) or 0)
|
|
202
|
+
payload.error_count = int(data.get("error_count", 0) or 0)
|
|
203
|
+
avg_ms = data.get("avg_duration_ms")
|
|
204
|
+
p95_ms = data.get("p95_duration_ms")
|
|
205
|
+
if avg_ms is not None:
|
|
206
|
+
payload.avg_duration_seconds = float(avg_ms) / 1000.0
|
|
207
|
+
if p95_ms is not None:
|
|
208
|
+
payload.p95_duration_seconds = float(p95_ms) / 1000.0
|
|
209
|
+
if payload.request_count > 0:
|
|
210
|
+
payload.error_rate = payload.error_count / payload.request_count
|
|
211
|
+
|
|
212
|
+
# Best-effort second pass: content-filter / safety triggers.
|
|
213
|
+
# Failures here are isolated from the primary metrics above.
|
|
214
|
+
try:
|
|
215
|
+
safety_kql = _SAFETY_KQL.format(lookback_days=int(lookback_days))
|
|
216
|
+
if config.log_analytics_workspace_id:
|
|
217
|
+
safety_response = client.query_workspace(
|
|
218
|
+
workspace_id=config.log_analytics_workspace_id,
|
|
219
|
+
query=safety_kql,
|
|
220
|
+
timespan=None,
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
safety_response = client.query_resource( # type: ignore[union-attr]
|
|
224
|
+
resource_id=config.app_insights_resource_id,
|
|
225
|
+
query=safety_kql,
|
|
226
|
+
timespan=None,
|
|
227
|
+
)
|
|
228
|
+
if getattr(safety_response, "status", None) == LogsQueryStatus.FAILURE:
|
|
229
|
+
diagnostics["safety_status"] = "error"
|
|
230
|
+
diagnostics["safety_reason"] = "query failed"
|
|
231
|
+
else:
|
|
232
|
+
safety_tables = getattr(safety_response, "tables", []) or []
|
|
233
|
+
hits = 0
|
|
234
|
+
if safety_tables:
|
|
235
|
+
safety_rows = list(safety_tables[0].rows)
|
|
236
|
+
if safety_rows:
|
|
237
|
+
cols = [
|
|
238
|
+
c.name if hasattr(c, "name") else str(c)
|
|
239
|
+
for c in safety_tables[0].columns
|
|
240
|
+
]
|
|
241
|
+
data = dict(zip(cols, safety_rows[0]))
|
|
242
|
+
hits = int(data.get("hits", 0) or 0)
|
|
243
|
+
diagnostics["safety_status"] = "ok"
|
|
244
|
+
diagnostics["safety_hits"] = hits
|
|
245
|
+
if hits > 0:
|
|
246
|
+
payload.safety_violations.append(
|
|
247
|
+
{"signal": "content_filter", "hits": hits}
|
|
248
|
+
)
|
|
249
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
250
|
+
diagnostics["safety_status"] = "error"
|
|
251
|
+
diagnostics["safety_reason"] = str(exc)
|
|
252
|
+
log.info("Safety KQL probe failed (non-fatal): %s", exc)
|
|
253
|
+
|
|
254
|
+
# AI.132 — token-usage probe. Non-fatal; populates payload.input/output_token_count.
|
|
255
|
+
try:
|
|
256
|
+
token_kql = _TOKEN_USAGE_KQL.format(lookback_days=int(lookback_days))
|
|
257
|
+
if config.log_analytics_workspace_id:
|
|
258
|
+
tok_response = client.query_workspace(
|
|
259
|
+
workspace_id=config.log_analytics_workspace_id,
|
|
260
|
+
query=token_kql,
|
|
261
|
+
timespan=None,
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
tok_response = client.query_resource( # type: ignore[union-attr]
|
|
265
|
+
resource_id=config.app_insights_resource_id,
|
|
266
|
+
query=token_kql,
|
|
267
|
+
timespan=None,
|
|
268
|
+
)
|
|
269
|
+
if getattr(tok_response, "status", None) != LogsQueryStatus.FAILURE:
|
|
270
|
+
tok_tables = getattr(tok_response, "tables", []) or []
|
|
271
|
+
if tok_tables:
|
|
272
|
+
tok_rows = list(tok_tables[0].rows)
|
|
273
|
+
if tok_rows:
|
|
274
|
+
cols = [
|
|
275
|
+
c.name if hasattr(c, "name") else str(c)
|
|
276
|
+
for c in tok_tables[0].columns
|
|
277
|
+
]
|
|
278
|
+
data = dict(zip(cols, tok_rows[0]))
|
|
279
|
+
in_t = data.get("input_tokens")
|
|
280
|
+
out_t = data.get("output_tokens")
|
|
281
|
+
payload.input_token_count = int(in_t) if in_t is not None else 0
|
|
282
|
+
payload.output_token_count = int(out_t) if out_t is not None else 0
|
|
283
|
+
diagnostics["token_status"] = "ok"
|
|
284
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
285
|
+
diagnostics["token_status"] = "error"
|
|
286
|
+
diagnostics["token_reason"] = str(exc)
|
|
287
|
+
log.info("Token-usage KQL probe failed (non-fatal): %s", exc)
|
|
288
|
+
|
|
289
|
+
# AI.154 — HTTP 429 (rate-limit) probe.
|
|
290
|
+
try:
|
|
291
|
+
rl_kql = _RATE_LIMIT_KQL.format(lookback_days=int(lookback_days))
|
|
292
|
+
if config.log_analytics_workspace_id:
|
|
293
|
+
rl_response = client.query_workspace(
|
|
294
|
+
workspace_id=config.log_analytics_workspace_id,
|
|
295
|
+
query=rl_kql,
|
|
296
|
+
timespan=None,
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
rl_response = client.query_resource( # type: ignore[union-attr]
|
|
300
|
+
resource_id=config.app_insights_resource_id,
|
|
301
|
+
query=rl_kql,
|
|
302
|
+
timespan=None,
|
|
303
|
+
)
|
|
304
|
+
if getattr(rl_response, "status", None) != LogsQueryStatus.FAILURE:
|
|
305
|
+
rl_tables = getattr(rl_response, "tables", []) or []
|
|
306
|
+
hits = 0
|
|
307
|
+
if rl_tables:
|
|
308
|
+
rl_rows = list(rl_tables[0].rows)
|
|
309
|
+
if rl_rows:
|
|
310
|
+
cols = [
|
|
311
|
+
c.name if hasattr(c, "name") else str(c)
|
|
312
|
+
for c in rl_tables[0].columns
|
|
313
|
+
]
|
|
314
|
+
data = dict(zip(cols, rl_rows[0]))
|
|
315
|
+
hits = int(data.get("hits", 0) or 0)
|
|
316
|
+
payload.rate_limit_429_count = hits
|
|
317
|
+
diagnostics["rate_limit_status"] = "ok"
|
|
318
|
+
diagnostics["rate_limit_hits"] = hits
|
|
319
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
320
|
+
diagnostics["rate_limit_status"] = "error"
|
|
321
|
+
diagnostics["rate_limit_reason"] = str(exc)
|
|
322
|
+
log.info("Rate-limit KQL probe failed (non-fatal): %s", exc)
|
|
323
|
+
|
|
324
|
+
return payload
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _resolve_application_id() -> tuple[Optional[str], Optional[str], Optional[str]]:
|
|
328
|
+
"""Resolve an App Insights ApplicationId for REST API queries."""
|
|
329
|
+
last_reason: Optional[str] = None
|
|
330
|
+
for env_name in (
|
|
331
|
+
"APPLICATIONINSIGHTS_CONNECTION_STRING",
|
|
332
|
+
"AGENTOPS_APPLICATIONINSIGHTS_CONNECTION_STRING",
|
|
333
|
+
):
|
|
334
|
+
connection_string = os.getenv(env_name)
|
|
335
|
+
if not connection_string:
|
|
336
|
+
continue
|
|
337
|
+
application_id = _extract_application_id(connection_string)
|
|
338
|
+
if application_id:
|
|
339
|
+
return application_id, env_name, None
|
|
340
|
+
last_reason = f"{env_name} has no ApplicationId segment"
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
from agentops.utils.foundry_discovery import (
|
|
344
|
+
resolve_appinsights_connection_from_env_with_reason,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
connection_string, reason = resolve_appinsights_connection_from_env_with_reason()
|
|
348
|
+
except Exception as exc: # noqa: BLE001
|
|
349
|
+
return None, None, f"Foundry App Insights discovery failed: {exc}"
|
|
350
|
+
|
|
351
|
+
if connection_string:
|
|
352
|
+
application_id = _extract_application_id(connection_string)
|
|
353
|
+
if application_id:
|
|
354
|
+
return application_id, "foundry_project_telemetry", None
|
|
355
|
+
return (
|
|
356
|
+
None,
|
|
357
|
+
None,
|
|
358
|
+
"Foundry App Insights connection string has no ApplicationId segment",
|
|
359
|
+
)
|
|
360
|
+
return None, None, reason or last_reason
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _extract_application_id(connection_string: Optional[str]) -> Optional[str]:
|
|
364
|
+
if not connection_string:
|
|
365
|
+
return None
|
|
366
|
+
match = re.search(r"(?:^|;)ApplicationId=([^;]+)", connection_string)
|
|
367
|
+
return match.group(1).strip() if match else None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _collect_application_insights_by_app_id(
|
|
371
|
+
application_id: str,
|
|
372
|
+
lookback_days: int,
|
|
373
|
+
diagnostics: Dict[str, Any],
|
|
374
|
+
) -> AzureMonitorPayload:
|
|
375
|
+
"""Query App Insights by ApplicationId when no ARM resource id is configured."""
|
|
376
|
+
try:
|
|
377
|
+
bearer = _acquire_application_insights_token()
|
|
378
|
+
except ImportError as exc:
|
|
379
|
+
diagnostics["status"] = "skipped"
|
|
380
|
+
diagnostics["reason"] = "azure-identity not installed (install agentops-accelerator[agent])"
|
|
381
|
+
log.info("azure-identity unavailable: %s", exc)
|
|
382
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
383
|
+
except Exception as exc: # pragma: no cover - network / auth errors
|
|
384
|
+
diagnostics["status"] = "error"
|
|
385
|
+
diagnostics["reason"] = str(exc)
|
|
386
|
+
log.warning("App Insights token acquisition failed: %s", exc)
|
|
387
|
+
return AzureMonitorPayload(diagnostics=diagnostics)
|
|
388
|
+
|
|
389
|
+
payload = AzureMonitorPayload(diagnostics=diagnostics)
|
|
390
|
+
kql = _REQUESTS_KQL.format(lookback_days=int(lookback_days))
|
|
391
|
+
summary = _query_application_insights(application_id, bearer, kql)
|
|
392
|
+
if summary is None:
|
|
393
|
+
diagnostics["status"] = "error"
|
|
394
|
+
diagnostics["reason"] = "Application Insights query failed"
|
|
395
|
+
return payload
|
|
396
|
+
|
|
397
|
+
diagnostics["status"] = "ok"
|
|
398
|
+
row = _first_rest_row(summary)
|
|
399
|
+
if row:
|
|
400
|
+
_apply_summary_row(payload, row)
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
safety_kql = _SAFETY_KQL.format(lookback_days=int(lookback_days))
|
|
404
|
+
safety = _query_application_insights(application_id, bearer, safety_kql)
|
|
405
|
+
if safety is None:
|
|
406
|
+
diagnostics["safety_status"] = "error"
|
|
407
|
+
diagnostics["safety_reason"] = "query failed"
|
|
408
|
+
else:
|
|
409
|
+
hits = int((_first_rest_row(safety) or {}).get("hits", 0) or 0)
|
|
410
|
+
diagnostics["safety_status"] = "ok"
|
|
411
|
+
diagnostics["safety_hits"] = hits
|
|
412
|
+
if hits > 0:
|
|
413
|
+
payload.safety_violations.append(
|
|
414
|
+
{"signal": "content_filter", "hits": hits}
|
|
415
|
+
)
|
|
416
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
417
|
+
diagnostics["safety_status"] = "error"
|
|
418
|
+
diagnostics["safety_reason"] = str(exc)
|
|
419
|
+
log.info("Safety App Insights probe failed (non-fatal): %s", exc)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
token_kql = _TOKEN_USAGE_KQL.format(lookback_days=int(lookback_days))
|
|
423
|
+
token = _query_application_insights(application_id, bearer, token_kql)
|
|
424
|
+
if token is None:
|
|
425
|
+
diagnostics["token_status"] = "error"
|
|
426
|
+
diagnostics["token_reason"] = "query failed"
|
|
427
|
+
else:
|
|
428
|
+
token_row = _first_rest_row(token) or {}
|
|
429
|
+
in_t = token_row.get("input_tokens")
|
|
430
|
+
out_t = token_row.get("output_tokens")
|
|
431
|
+
payload.input_token_count = int(in_t) if in_t is not None else 0
|
|
432
|
+
payload.output_token_count = int(out_t) if out_t is not None else 0
|
|
433
|
+
diagnostics["token_status"] = "ok"
|
|
434
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
435
|
+
diagnostics["token_status"] = "error"
|
|
436
|
+
diagnostics["token_reason"] = str(exc)
|
|
437
|
+
log.info("Token-usage App Insights probe failed (non-fatal): %s", exc)
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
rl_kql = _RATE_LIMIT_KQL.format(lookback_days=int(lookback_days))
|
|
441
|
+
rate_limit = _query_application_insights(application_id, bearer, rl_kql)
|
|
442
|
+
if rate_limit is None:
|
|
443
|
+
diagnostics["rate_limit_status"] = "error"
|
|
444
|
+
diagnostics["rate_limit_reason"] = "query failed"
|
|
445
|
+
else:
|
|
446
|
+
hits = int((_first_rest_row(rate_limit) or {}).get("hits", 0) or 0)
|
|
447
|
+
payload.rate_limit_429_count = hits
|
|
448
|
+
diagnostics["rate_limit_status"] = "ok"
|
|
449
|
+
diagnostics["rate_limit_hits"] = hits
|
|
450
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
451
|
+
diagnostics["rate_limit_status"] = "error"
|
|
452
|
+
diagnostics["rate_limit_reason"] = str(exc)
|
|
453
|
+
log.info("Rate-limit App Insights probe failed (non-fatal): %s", exc)
|
|
454
|
+
|
|
455
|
+
return payload
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _acquire_application_insights_token() -> str:
|
|
459
|
+
from azure.identity import DefaultAzureCredential
|
|
460
|
+
|
|
461
|
+
credential = DefaultAzureCredential(
|
|
462
|
+
exclude_developer_cli_credential=True,
|
|
463
|
+
process_timeout=30,
|
|
464
|
+
)
|
|
465
|
+
token = credential.get_token("https://api.applicationinsights.io/.default")
|
|
466
|
+
return token.token
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _query_application_insights(
|
|
470
|
+
application_id: str,
|
|
471
|
+
bearer: str,
|
|
472
|
+
kql: str,
|
|
473
|
+
) -> Optional[Dict[str, Any]]:
|
|
474
|
+
body = json.dumps({"query": kql}).encode("utf-8")
|
|
475
|
+
req = request.Request(
|
|
476
|
+
url=f"https://api.applicationinsights.io/v1/apps/{application_id}/query",
|
|
477
|
+
data=body,
|
|
478
|
+
headers={
|
|
479
|
+
"Authorization": f"Bearer {bearer}",
|
|
480
|
+
"Content-Type": "application/json",
|
|
481
|
+
},
|
|
482
|
+
method="POST",
|
|
483
|
+
)
|
|
484
|
+
try:
|
|
485
|
+
with request.urlopen(req, timeout=10) as resp: # noqa: S310
|
|
486
|
+
parsed = json.loads(resp.read())
|
|
487
|
+
except (error.URLError, ValueError, KeyError) as exc:
|
|
488
|
+
log.debug("App Insights REST query failed: %s", exc)
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
if isinstance(parsed, dict) and parsed.get("error"):
|
|
492
|
+
err = parsed["error"]
|
|
493
|
+
msg = err.get("message") if isinstance(err, dict) else str(err)
|
|
494
|
+
log.debug("App Insights REST query reported error: %s", msg)
|
|
495
|
+
return None
|
|
496
|
+
return parsed
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _first_rest_row(result: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
|
500
|
+
if not result:
|
|
501
|
+
return None
|
|
502
|
+
tables = result.get("tables") or []
|
|
503
|
+
if not tables:
|
|
504
|
+
return None
|
|
505
|
+
table = tables[0]
|
|
506
|
+
columns = [col.get("name") for col in (table.get("columns") or [])]
|
|
507
|
+
rows = table.get("rows") or []
|
|
508
|
+
if not rows:
|
|
509
|
+
return None
|
|
510
|
+
return dict(zip(columns, rows[0]))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _apply_summary_row(payload: AzureMonitorPayload, data: Dict[str, Any]) -> None:
|
|
514
|
+
payload.request_count = int(data.get("request_count", 0) or 0)
|
|
515
|
+
payload.error_count = int(data.get("error_count", 0) or 0)
|
|
516
|
+
avg_ms = data.get("avg_duration_ms")
|
|
517
|
+
p95_ms = data.get("p95_duration_ms")
|
|
518
|
+
if avg_ms is not None:
|
|
519
|
+
payload.avg_duration_seconds = float(avg_ms) / 1000.0
|
|
520
|
+
if p95_ms is not None:
|
|
521
|
+
payload.p95_duration_seconds = float(p95_ms) / 1000.0
|
|
522
|
+
if payload.request_count > 0:
|
|
523
|
+
payload.error_rate = payload.error_count / payload.request_count
|