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.
Files changed (142) hide show
  1. agentops/__init__.py +10 -0
  2. agentops/__main__.py +6 -0
  3. agentops/agent/__init__.py +12 -0
  4. agentops/agent/_legacy_ids.py +92 -0
  5. agentops/agent/analyzer.py +207 -0
  6. agentops/agent/checks/__init__.py +1 -0
  7. agentops/agent/checks/catalog.py +880 -0
  8. agentops/agent/checks/errors.py +279 -0
  9. agentops/agent/checks/foundry_config.py +75 -0
  10. agentops/agent/checks/latency.py +84 -0
  11. agentops/agent/checks/opex.py +157 -0
  12. agentops/agent/checks/opex_workspace.py +874 -0
  13. agentops/agent/checks/posture.py +36 -0
  14. agentops/agent/checks/posture_rules/__init__.py +53 -0
  15. agentops/agent/checks/posture_rules/content_filter.py +59 -0
  16. agentops/agent/checks/posture_rules/diagnostics.py +74 -0
  17. agentops/agent/checks/posture_rules/local_auth.py +55 -0
  18. agentops/agent/checks/posture_rules/managed_identity.py +59 -0
  19. agentops/agent/checks/posture_rules/network.py +68 -0
  20. agentops/agent/checks/regression.py +78 -0
  21. agentops/agent/checks/release_readiness.py +182 -0
  22. agentops/agent/checks/safety.py +247 -0
  23. agentops/agent/checks/spec_conformance.py +375 -0
  24. agentops/agent/cockpit.py +5159 -0
  25. agentops/agent/config.py +240 -0
  26. agentops/agent/findings.py +113 -0
  27. agentops/agent/history.py +142 -0
  28. agentops/agent/knowledge/__init__.py +182 -0
  29. agentops/agent/knowledge/waf-checklist.csv +39 -0
  30. agentops/agent/llm_assist/__init__.py +16 -0
  31. agentops/agent/llm_assist/_base.py +124 -0
  32. agentops/agent/llm_assist/_bundle_rule.py +154 -0
  33. agentops/agent/llm_assist/_client.py +347 -0
  34. agentops/agent/llm_assist/_dataset_rules.py +191 -0
  35. agentops/agent/llm_assist/_engine.py +106 -0
  36. agentops/agent/llm_assist/_prompt_rules.py +291 -0
  37. agentops/agent/llm_assist/_spec_rules.py +235 -0
  38. agentops/agent/production_telemetry.py +430 -0
  39. agentops/agent/report.py +207 -0
  40. agentops/agent/server/__init__.py +1 -0
  41. agentops/agent/server/app.py +84 -0
  42. agentops/agent/server/auth.py +94 -0
  43. agentops/agent/server/chat.py +44 -0
  44. agentops/agent/server/protocol.py +72 -0
  45. agentops/agent/sources/__init__.py +1 -0
  46. agentops/agent/sources/azure_monitor.py +523 -0
  47. agentops/agent/sources/azure_resources.py +602 -0
  48. agentops/agent/sources/foundry_control.py +174 -0
  49. agentops/agent/sources/results_history.py +494 -0
  50. agentops/agent/sources/spec_detectors/__init__.py +42 -0
  51. agentops/agent/sources/spec_detectors/_base.py +58 -0
  52. agentops/agent/sources/spec_detectors/agents_md.py +75 -0
  53. agentops/agent/sources/spec_detectors/spec_kit.py +172 -0
  54. agentops/agent/time_range.py +117 -0
  55. agentops/cli/__init__.py +1 -0
  56. agentops/cli/app.py +4823 -0
  57. agentops/core/__init__.py +1 -0
  58. agentops/core/agentops_config.py +592 -0
  59. agentops/core/config_loader.py +22 -0
  60. agentops/core/evaluators.py +480 -0
  61. agentops/core/release_evidence.py +56 -0
  62. agentops/core/results.py +117 -0
  63. agentops/mcp/__init__.py +10 -0
  64. agentops/mcp/server.py +232 -0
  65. agentops/pipeline/__init__.py +8 -0
  66. agentops/pipeline/cloud_results.py +189 -0
  67. agentops/pipeline/cloud_runner.py +901 -0
  68. agentops/pipeline/comparison.py +108 -0
  69. agentops/pipeline/diagnostics.py +51 -0
  70. agentops/pipeline/invocations.py +535 -0
  71. agentops/pipeline/official_eval.py +414 -0
  72. agentops/pipeline/orchestrator.py +775 -0
  73. agentops/pipeline/prompt_deploy.py +377 -0
  74. agentops/pipeline/publisher.py +121 -0
  75. agentops/pipeline/reporter.py +202 -0
  76. agentops/pipeline/runtime.py +409 -0
  77. agentops/pipeline/thresholds.py +84 -0
  78. agentops/services/__init__.py +1 -0
  79. agentops/services/cicd.py +720 -0
  80. agentops/services/eval_analysis.py +848 -0
  81. agentops/services/evidence_pack.py +757 -0
  82. agentops/services/initializer.py +86 -0
  83. agentops/services/preflight.py +470 -0
  84. agentops/services/setup_wizard.py +709 -0
  85. agentops/services/skills.py +643 -0
  86. agentops/services/trace_promotion.py +300 -0
  87. agentops/services/workflow_analysis.py +1129 -0
  88. agentops/templates/.gitignore +15 -0
  89. agentops/templates/__init__.py +1 -0
  90. agentops/templates/agent-server/Dockerfile +23 -0
  91. agentops/templates/agent-server/README.md +61 -0
  92. agentops/templates/agent-server/main.bicep +94 -0
  93. agentops/templates/agent.yaml +87 -0
  94. agentops/templates/agentops.yaml +58 -0
  95. agentops/templates/foundry.svg +71 -0
  96. agentops/templates/icon.png +0 -0
  97. agentops/templates/pipelines/azuredevops/agentops-deploy-dev-azd.yml +118 -0
  98. agentops/templates/pipelines/azuredevops/agentops-deploy-dev.yml +73 -0
  99. agentops/templates/pipelines/azuredevops/agentops-deploy-prod-azd.yml +141 -0
  100. agentops/templates/pipelines/azuredevops/agentops-deploy-prod.yml +94 -0
  101. agentops/templates/pipelines/azuredevops/agentops-deploy-prompt-agent.yml +167 -0
  102. agentops/templates/pipelines/azuredevops/agentops-deploy-qa-azd.yml +118 -0
  103. agentops/templates/pipelines/azuredevops/agentops-deploy-qa.yml +68 -0
  104. agentops/templates/pipelines/azuredevops/agentops-pr-prompt-agent.yml +210 -0
  105. agentops/templates/pipelines/azuredevops/agentops-pr.yml +155 -0
  106. agentops/templates/pipelines/azuredevops/agentops-watchdog.yml +106 -0
  107. agentops/templates/project.gitignore +36 -0
  108. agentops/templates/sample-traces.jsonl +3 -0
  109. agentops/templates/skills/agentops-agent/SKILL.md +137 -0
  110. agentops/templates/skills/agentops-config/SKILL.md +113 -0
  111. agentops/templates/skills/agentops-dataset/SKILL.md +84 -0
  112. agentops/templates/skills/agentops-eval/SKILL.md +189 -0
  113. agentops/templates/skills/agentops-report/SKILL.md +71 -0
  114. agentops/templates/skills/agentops-workflow/SKILL.md +471 -0
  115. agentops/templates/smoke.jsonl +3 -0
  116. agentops/templates/waf-checklist.README.md +84 -0
  117. agentops/templates/waf-checklist.csv +22 -0
  118. agentops/templates/workflows/agentops-deploy-dev-azd.yml +166 -0
  119. agentops/templates/workflows/agentops-deploy-dev.yml +187 -0
  120. agentops/templates/workflows/agentops-deploy-prod-azd.yml +183 -0
  121. agentops/templates/workflows/agentops-deploy-prod.yml +171 -0
  122. agentops/templates/workflows/agentops-deploy-prompt-agent.yml +197 -0
  123. agentops/templates/workflows/agentops-deploy-qa-azd.yml +156 -0
  124. agentops/templates/workflows/agentops-deploy-qa.yml +145 -0
  125. agentops/templates/workflows/agentops-pr-prompt-agent.yml +210 -0
  126. agentops/templates/workflows/agentops-pr.yml +148 -0
  127. agentops/templates/workflows/agentops-watchdog.yml +122 -0
  128. agentops/utils/__init__.py +1 -0
  129. agentops/utils/azd_env.py +435 -0
  130. agentops/utils/azure_endpoints.py +62 -0
  131. agentops/utils/colors.py +47 -0
  132. agentops/utils/dotenv_loader.py +105 -0
  133. agentops/utils/foundry_discovery.py +229 -0
  134. agentops/utils/logging.py +59 -0
  135. agentops/utils/telemetry.py +554 -0
  136. agentops/utils/yaml.py +36 -0
  137. agentops_accelerator-0.3.0.dist-info/METADATA +278 -0
  138. agentops_accelerator-0.3.0.dist-info/RECORD +142 -0
  139. agentops_accelerator-0.3.0.dist-info/WHEEL +5 -0
  140. agentops_accelerator-0.3.0.dist-info/entry_points.txt +2 -0
  141. agentops_accelerator-0.3.0.dist-info/licenses/LICENSE +21 -0
  142. 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