holmesgpt 0.11.5__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.

Potentially problematic release.


This version of holmesgpt might be problematic. Click here for more details.

Files changed (183) hide show
  1. holmes/.git_archival.json +7 -0
  2. holmes/__init__.py +76 -0
  3. holmes/__init__.py.bak +76 -0
  4. holmes/clients/robusta_client.py +24 -0
  5. holmes/common/env_vars.py +47 -0
  6. holmes/config.py +526 -0
  7. holmes/core/__init__.py +0 -0
  8. holmes/core/conversations.py +578 -0
  9. holmes/core/investigation.py +152 -0
  10. holmes/core/investigation_structured_output.py +264 -0
  11. holmes/core/issue.py +54 -0
  12. holmes/core/llm.py +250 -0
  13. holmes/core/models.py +157 -0
  14. holmes/core/openai_formatting.py +51 -0
  15. holmes/core/performance_timing.py +72 -0
  16. holmes/core/prompt.py +42 -0
  17. holmes/core/resource_instruction.py +17 -0
  18. holmes/core/runbooks.py +26 -0
  19. holmes/core/safeguards.py +120 -0
  20. holmes/core/supabase_dal.py +540 -0
  21. holmes/core/tool_calling_llm.py +798 -0
  22. holmes/core/tools.py +566 -0
  23. holmes/core/tools_utils/__init__.py +0 -0
  24. holmes/core/tools_utils/tool_executor.py +65 -0
  25. holmes/core/tools_utils/toolset_utils.py +52 -0
  26. holmes/core/toolset_manager.py +418 -0
  27. holmes/interactive.py +229 -0
  28. holmes/main.py +1041 -0
  29. holmes/plugins/__init__.py +0 -0
  30. holmes/plugins/destinations/__init__.py +6 -0
  31. holmes/plugins/destinations/slack/__init__.py +2 -0
  32. holmes/plugins/destinations/slack/plugin.py +163 -0
  33. holmes/plugins/interfaces.py +32 -0
  34. holmes/plugins/prompts/__init__.py +48 -0
  35. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  36. holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
  37. holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
  38. holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
  39. holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
  41. holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
  42. holmes/plugins/prompts/generic_ask.jinja2 +36 -0
  43. holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
  44. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
  45. holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
  46. holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
  47. holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
  48. holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
  49. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
  50. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
  51. holmes/plugins/runbooks/README.md +22 -0
  52. holmes/plugins/runbooks/__init__.py +100 -0
  53. holmes/plugins/runbooks/catalog.json +14 -0
  54. holmes/plugins/runbooks/jira.yaml +12 -0
  55. holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
  56. holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
  57. holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
  58. holmes/plugins/sources/github/__init__.py +77 -0
  59. holmes/plugins/sources/jira/__init__.py +123 -0
  60. holmes/plugins/sources/opsgenie/__init__.py +93 -0
  61. holmes/plugins/sources/pagerduty/__init__.py +147 -0
  62. holmes/plugins/sources/prometheus/__init__.py +0 -0
  63. holmes/plugins/sources/prometheus/models.py +104 -0
  64. holmes/plugins/sources/prometheus/plugin.py +154 -0
  65. holmes/plugins/toolsets/__init__.py +171 -0
  66. holmes/plugins/toolsets/aks-node-health.yaml +65 -0
  67. holmes/plugins/toolsets/aks.yaml +86 -0
  68. holmes/plugins/toolsets/argocd.yaml +70 -0
  69. holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
  70. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
  71. holmes/plugins/toolsets/aws.yaml +76 -0
  72. holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
  73. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
  74. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
  75. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
  76. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
  77. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
  78. holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
  79. holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
  80. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
  81. holmes/plugins/toolsets/azure_sql/install.md +66 -0
  82. holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
  83. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
  84. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
  85. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
  86. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
  87. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
  88. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
  89. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
  90. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
  91. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
  92. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
  93. holmes/plugins/toolsets/azure_sql/utils.py +83 -0
  94. holmes/plugins/toolsets/bash/__init__.py +0 -0
  95. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
  96. holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
  97. holmes/plugins/toolsets/bash/common/bash.py +52 -0
  98. holmes/plugins/toolsets/bash/common/config.py +14 -0
  99. holmes/plugins/toolsets/bash/common/stringify.py +25 -0
  100. holmes/plugins/toolsets/bash/common/validators.py +24 -0
  101. holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
  102. holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
  103. holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
  104. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
  105. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
  106. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
  107. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
  108. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
  109. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
  110. holmes/plugins/toolsets/bash/parse_command.py +103 -0
  111. holmes/plugins/toolsets/confluence.yaml +19 -0
  112. holmes/plugins/toolsets/consts.py +5 -0
  113. holmes/plugins/toolsets/coralogix/api.py +158 -0
  114. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
  115. holmes/plugins/toolsets/coralogix/utils.py +181 -0
  116. holmes/plugins/toolsets/datadog.py +153 -0
  117. holmes/plugins/toolsets/docker.yaml +46 -0
  118. holmes/plugins/toolsets/git.py +756 -0
  119. holmes/plugins/toolsets/grafana/__init__.py +0 -0
  120. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
  121. holmes/plugins/toolsets/grafana/common.py +68 -0
  122. holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
  123. holmes/plugins/toolsets/grafana/loki_api.py +89 -0
  124. holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
  125. holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
  126. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
  127. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
  128. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
  129. holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
  130. holmes/plugins/toolsets/helm.yaml +42 -0
  131. holmes/plugins/toolsets/internet/internet.py +275 -0
  132. holmes/plugins/toolsets/internet/notion.py +137 -0
  133. holmes/plugins/toolsets/kafka.py +638 -0
  134. holmes/plugins/toolsets/kubernetes.yaml +255 -0
  135. holmes/plugins/toolsets/kubernetes_logs.py +426 -0
  136. holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
  137. holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
  138. holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
  139. holmes/plugins/toolsets/logging_utils/types.py +0 -0
  140. holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
  141. holmes/plugins/toolsets/newrelic.py +222 -0
  142. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  143. holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
  144. holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
  145. holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
  146. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
  147. holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
  148. holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
  149. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
  150. holmes/plugins/toolsets/rabbitmq/api.py +398 -0
  151. holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
  152. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
  153. holmes/plugins/toolsets/robusta/__init__.py +0 -0
  154. holmes/plugins/toolsets/robusta/robusta.py +235 -0
  155. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
  156. holmes/plugins/toolsets/runbook/__init__.py +0 -0
  157. holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
  158. holmes/plugins/toolsets/service_discovery.py +92 -0
  159. holmes/plugins/toolsets/servicenow/install.md +37 -0
  160. holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
  161. holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
  162. holmes/plugins/toolsets/slab.yaml +20 -0
  163. holmes/plugins/toolsets/utils.py +137 -0
  164. holmes/plugins/utils.py +14 -0
  165. holmes/utils/__init__.py +0 -0
  166. holmes/utils/cache.py +84 -0
  167. holmes/utils/cert_utils.py +40 -0
  168. holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
  169. holmes/utils/definitions.py +13 -0
  170. holmes/utils/env.py +53 -0
  171. holmes/utils/file_utils.py +56 -0
  172. holmes/utils/global_instructions.py +20 -0
  173. holmes/utils/holmes_status.py +22 -0
  174. holmes/utils/holmes_sync_toolsets.py +80 -0
  175. holmes/utils/markdown_utils.py +55 -0
  176. holmes/utils/pydantic_utils.py +54 -0
  177. holmes/utils/robusta.py +10 -0
  178. holmes/utils/tags.py +97 -0
  179. holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
  180. holmesgpt-0.11.5.dist-info/METADATA +400 -0
  181. holmesgpt-0.11.5.dist-info/RECORD +183 -0
  182. holmesgpt-0.11.5.dist-info/WHEEL +4 -0
  183. holmesgpt-0.11.5.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,205 @@
1
+ import logging
2
+ from typing import Dict
3
+ from datetime import datetime, timezone
4
+
5
+ from holmes.core.tools import StructuredToolResult, ToolResultStatus
6
+ from holmes.plugins.toolsets.azure_sql.azure_base_toolset import (
7
+ BaseAzureSQLTool,
8
+ BaseAzureSQLToolset,
9
+ AzureSQLDatabaseConfig,
10
+ )
11
+ from holmes.plugins.toolsets.azure_sql.apis.azure_sql_api import AzureSQLAPIClient
12
+ from typing import Tuple
13
+
14
+
15
+ class AnalyzeDatabaseHealthStatus(BaseAzureSQLTool):
16
+ def __init__(self, toolset: "BaseAzureSQLToolset"):
17
+ super().__init__(
18
+ name="analyze_database_health_status",
19
+ description="Analyzes the overall health status of an Azure SQL database including active operations, resource usage alerts, and system status. Use this first to get a high-level view of database health.",
20
+ parameters={},
21
+ toolset=toolset,
22
+ )
23
+
24
+ def _gather_health_data(
25
+ self, db_config: AzureSQLDatabaseConfig, client: AzureSQLAPIClient
26
+ ) -> Dict:
27
+ """Gather health-related data from Azure SQL API."""
28
+ health_data = {
29
+ "database_info": {
30
+ "name": db_config.database_name,
31
+ "server": db_config.server_name,
32
+ "resource_group": db_config.resource_group,
33
+ "subscription_id": db_config.subscription_id,
34
+ },
35
+ "timestamp": datetime.now(timezone.utc).isoformat(),
36
+ }
37
+
38
+ try:
39
+ operations = client.get_database_operations(
40
+ db_config.subscription_id,
41
+ db_config.resource_group,
42
+ db_config.server_name,
43
+ db_config.database_name,
44
+ )
45
+ health_data["operations"] = operations.get("value", [])
46
+ except Exception as e:
47
+ health_data["operations_error"] = str(e)
48
+
49
+ try:
50
+ usages = client.get_database_usages(
51
+ db_config.subscription_id,
52
+ db_config.resource_group,
53
+ db_config.server_name,
54
+ db_config.database_name,
55
+ )
56
+ health_data["resource_usage"] = usages.get("value", [])
57
+ except Exception as e:
58
+ health_data["usage_error"] = str(e)
59
+
60
+ return health_data
61
+
62
+ def _build_health_report(
63
+ self, health_data: Dict, db_config: AzureSQLDatabaseConfig
64
+ ) -> str:
65
+ """Build the formatted health report from gathered data."""
66
+ report_sections = []
67
+
68
+ # Database Overview Section
69
+ report_sections.append("# Azure SQL Database Health Report")
70
+ report_sections.append(f"**Database:** {db_config.database_name}")
71
+ report_sections.append(f"**Server:** {db_config.server_name}")
72
+ report_sections.append(f"**Generated:** {health_data['timestamp']}")
73
+ report_sections.append("")
74
+
75
+ # Operations Status Section
76
+ report_sections.append("## Operations Status")
77
+ if "operations_error" in health_data:
78
+ report_sections.append(
79
+ f"⚠️ **Error retrieving operations:** {health_data['operations_error']}"
80
+ )
81
+ else:
82
+ operations = health_data.get("operations", [])
83
+ if operations:
84
+ report_sections.append(f"**Active Operations:** {len(operations)}")
85
+ for op in operations[:5]: # Show first 5 operations
86
+ status = op.get("properties", {}).get("state", "Unknown")
87
+ op_type = op.get("properties", {}).get("operation", "Unknown")
88
+ start_time = op.get("properties", {}).get("startTime", "Unknown")
89
+ report_sections.append(
90
+ f"- **{op_type}**: {status} (Started: {start_time})"
91
+ )
92
+ else:
93
+ report_sections.append("✅ **No active operations**")
94
+ report_sections.append("")
95
+
96
+ # Resource Usage Section
97
+ report_sections.append("## Resource Usage")
98
+ if "usage_error" in health_data:
99
+ report_sections.append(
100
+ f"⚠️ **Error retrieving usage data:** {health_data['usage_error']}"
101
+ )
102
+ else:
103
+ usages = health_data.get("resource_usage", [])
104
+ if usages:
105
+ for usage in usages:
106
+ name = usage.get(
107
+ "display_name", usage.get("displayName", "Unknown Metric")
108
+ )
109
+ current = usage.get("current_value", usage.get("currentValue", 0))
110
+ limit = usage.get("limit", 0)
111
+ unit = usage.get("unit", "")
112
+
113
+ if limit > 0:
114
+ percentage = (current / limit) * 100
115
+ status_icon = (
116
+ "🔴"
117
+ if percentage > 90
118
+ else "🟡"
119
+ if percentage > 70
120
+ else "🟢"
121
+ )
122
+ report_sections.append(
123
+ f"- **{name}**: {status_icon} {current:,} / {limit:,} {unit} ({percentage:.1f}%)"
124
+ )
125
+ else:
126
+ report_sections.append(f"- **{name}**: {current:,} {unit}")
127
+ else:
128
+ report_sections.append("No usage data available")
129
+
130
+ return "\n".join(report_sections)
131
+
132
+ def _invoke(self, params: Dict) -> StructuredToolResult:
133
+ try:
134
+ db_config = self.toolset.database_config()
135
+ client = self.toolset.api_client()
136
+
137
+ # Gather health-related data
138
+ health_data = self._gather_health_data(db_config, client)
139
+
140
+ # Build the formatted report
141
+ report_text = self._build_health_report(health_data, db_config)
142
+
143
+ return StructuredToolResult(
144
+ status=ToolResultStatus.SUCCESS,
145
+ data=report_text,
146
+ params=params,
147
+ )
148
+ except Exception as e:
149
+ error_msg = f"Failed to generate health report: {str(e)}"
150
+ logging.error(error_msg)
151
+ return StructuredToolResult(
152
+ status=ToolResultStatus.ERROR,
153
+ error=error_msg,
154
+ params=params,
155
+ )
156
+
157
+ def get_parameterized_one_liner(self, params: Dict) -> str:
158
+ db_config = self.toolset.database_config()
159
+ return f"Analyze health status for database {db_config.server_name}/{db_config.database_name}"
160
+
161
+ @staticmethod
162
+ def validate_config(
163
+ api_client: AzureSQLAPIClient, database_config: AzureSQLDatabaseConfig
164
+ ) -> Tuple[bool, str]:
165
+ errors = []
166
+
167
+ try:
168
+ # Test database operations API access
169
+ api_client.get_database_operations(
170
+ database_config.subscription_id,
171
+ database_config.resource_group,
172
+ database_config.server_name,
173
+ database_config.database_name,
174
+ )
175
+ except Exception as e:
176
+ error_msg = str(e)
177
+ if (
178
+ "authorization" in error_msg.lower()
179
+ or "permission" in error_msg.lower()
180
+ ):
181
+ errors.append(f"Database operations access denied: {error_msg}")
182
+ else:
183
+ errors.append(f"Database operations connection failed: {error_msg}")
184
+
185
+ try:
186
+ # Test database usages API access
187
+ api_client.get_database_usages(
188
+ database_config.subscription_id,
189
+ database_config.resource_group,
190
+ database_config.server_name,
191
+ database_config.database_name,
192
+ )
193
+ except Exception as e:
194
+ error_msg = str(e)
195
+ if (
196
+ "authorization" in error_msg.lower()
197
+ or "permission" in error_msg.lower()
198
+ ):
199
+ errors.append(f"Database usage metrics access denied: {error_msg}")
200
+ else:
201
+ errors.append(f"Database usage metrics connection failed: {error_msg}")
202
+
203
+ if errors:
204
+ return False, "\n".join(errors)
205
+ return True, ""
@@ -0,0 +1,249 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Tuple, cast
3
+ from datetime import datetime, timezone
4
+
5
+ from holmes.core.tools import StructuredToolResult, ToolResultStatus
6
+ from holmes.plugins.toolsets.azure_sql.azure_base_toolset import (
7
+ BaseAzureSQLTool,
8
+ BaseAzureSQLToolset,
9
+ AzureSQLDatabaseConfig,
10
+ )
11
+ from holmes.plugins.toolsets.azure_sql.apis.azure_sql_api import AzureSQLAPIClient
12
+
13
+
14
+ class AnalyzeDatabasePerformance(BaseAzureSQLTool):
15
+ def __init__(self, toolset: "BaseAzureSQLToolset"):
16
+ super().__init__(
17
+ name="analyze_database_performance",
18
+ description="Analyzes database performance including automatic tuning status, performance advisors, and active recommendations. Essential for identifying performance optimization opportunities.",
19
+ parameters={},
20
+ toolset=toolset,
21
+ )
22
+
23
+ def _gather_performance_data(
24
+ self, db_config: AzureSQLDatabaseConfig, client: AzureSQLAPIClient
25
+ ) -> Dict:
26
+ """Gather performance-related data from Azure SQL API."""
27
+ performance_data = {
28
+ "database_info": {
29
+ "name": db_config.database_name,
30
+ "server": db_config.server_name,
31
+ "resource_group": db_config.resource_group,
32
+ },
33
+ "timestamp": datetime.now(timezone.utc).isoformat(),
34
+ }
35
+
36
+ try:
37
+ advisors = client.get_database_advisors(
38
+ db_config.subscription_id,
39
+ db_config.resource_group,
40
+ db_config.server_name,
41
+ db_config.database_name,
42
+ )
43
+ performance_data["advisors"] = advisors.get("value", [])
44
+ except Exception as e:
45
+ performance_data["advisors_error"] = str(e)
46
+
47
+ try:
48
+ auto_tuning = client.get_database_automatic_tuning(
49
+ db_config.subscription_id,
50
+ db_config.resource_group,
51
+ db_config.server_name,
52
+ db_config.database_name,
53
+ )
54
+ performance_data["automatic_tuning"] = auto_tuning
55
+ except Exception as e:
56
+ performance_data["auto_tuning_error"] = str(e)
57
+
58
+ # Get recommendations for each advisor
59
+ recommendations_list: List[Dict[str, Any]] = []
60
+ performance_data["recommendations"] = cast(Any, recommendations_list)
61
+ if "advisors" in performance_data:
62
+ for advisor in performance_data["advisors"]:
63
+ if isinstance(advisor, dict):
64
+ advisor_name = advisor.get("name", "")
65
+ else:
66
+ advisor_name = str(advisor)
67
+ try:
68
+ recommendations = client.get_database_recommended_actions(
69
+ db_config.subscription_id,
70
+ db_config.resource_group,
71
+ db_config.server_name,
72
+ db_config.database_name,
73
+ advisor_name,
74
+ )
75
+ recommendations_list.extend(recommendations.get("value", []))
76
+ except Exception as e:
77
+ logging.warning(
78
+ f"Failed to get recommendations for advisor {advisor_name}: {e}"
79
+ )
80
+
81
+ return performance_data
82
+
83
+ def _build_performance_report(
84
+ self, performance_data: Dict, db_config: AzureSQLDatabaseConfig
85
+ ) -> str:
86
+ """Build the formatted performance report from gathered data."""
87
+ report_sections = []
88
+
89
+ # Performance Report Header
90
+ report_sections.append("# Azure SQL Database Performance Report")
91
+ report_sections.append(f"**Database:** {db_config.database_name}")
92
+ report_sections.append(f"**Server:** {db_config.server_name}")
93
+ report_sections.append(f"**Generated:** {performance_data['timestamp']}")
94
+ report_sections.append("")
95
+
96
+ # Automatic Tuning Section
97
+ report_sections.append("## Automatic Tuning Status")
98
+ if "auto_tuning_error" in performance_data:
99
+ report_sections.append(
100
+ f"⚠️ **Error retrieving auto-tuning data:** {performance_data['auto_tuning_error']}"
101
+ )
102
+ else:
103
+ auto_tuning = performance_data.get("automatic_tuning", {})
104
+ # Handle both camelCase and snake_case field names
105
+ desired_state = auto_tuning.get(
106
+ "desired_state", auto_tuning.get("desiredState", "Unknown")
107
+ )
108
+ actual_state = auto_tuning.get(
109
+ "actual_state", auto_tuning.get("actualState", "Unknown")
110
+ )
111
+
112
+ status_icon = "✅" if desired_state == actual_state else "⚠️"
113
+ report_sections.append(f"- **Desired State**: {desired_state}")
114
+ report_sections.append(f"- **Actual State**: {actual_state} {status_icon}")
115
+
116
+ options = auto_tuning.get("options", {})
117
+ for option_name, option_data in options.items():
118
+ desired = option_data.get(
119
+ "desired_state", option_data.get("desiredState", "Unknown")
120
+ )
121
+ actual = option_data.get(
122
+ "actual_state", option_data.get("actualState", "Unknown")
123
+ )
124
+ option_icon = "✅" if desired == actual else "⚠️"
125
+ report_sections.append(f" - **{option_name}**: {actual} {option_icon}")
126
+ report_sections.append("")
127
+
128
+ # Performance Advisors Section
129
+ report_sections.append("## Performance Advisors")
130
+ if "advisors_error" in performance_data:
131
+ report_sections.append(
132
+ f"⚠️ **Error retrieving advisors:** {performance_data['advisors_error']}"
133
+ )
134
+ else:
135
+ advisors = performance_data.get("advisors", [])
136
+ if advisors:
137
+ for advisor in advisors:
138
+ name = advisor.get("name", "Unknown")
139
+ # Handle both camelCase and snake_case field names
140
+ auto_execute = advisor.get(
141
+ "auto_execute_status",
142
+ advisor.get("autoExecuteStatus", "Unknown"),
143
+ )
144
+ last_checked = advisor.get(
145
+ "last_checked", advisor.get("lastChecked", "Never")
146
+ )
147
+
148
+ report_sections.append(f"### {name}")
149
+ report_sections.append(f"- **Auto Execute**: {auto_execute}")
150
+ report_sections.append(f"- **Last Checked**: {last_checked}")
151
+ else:
152
+ report_sections.append("No performance advisors available")
153
+ report_sections.append("")
154
+
155
+ # Recommendations Section
156
+ report_sections.append("## Performance Recommendations")
157
+ all_recommendations = performance_data.get("recommendations", [])
158
+ if all_recommendations:
159
+ active_recommendations = [
160
+ r
161
+ for r in all_recommendations
162
+ if r.get("properties", {}).get("state", {}).get("currentValue")
163
+ in ["Active", "Pending"]
164
+ ]
165
+
166
+ if active_recommendations:
167
+ report_sections.append(
168
+ f"🚨 **{len(active_recommendations)} Active Recommendations Found**"
169
+ )
170
+ for rec in active_recommendations[:5]: # Show first 5 recommendations
171
+ properties = rec.get("properties", {})
172
+ details = properties.get("details", {})
173
+
174
+ rec_type = details.get("indexType", "Performance")
175
+ impact = details.get("impactDetails", [{}])[0].get(
176
+ "name", "Unknown"
177
+ )
178
+ state = properties.get("state", {}).get("currentValue", "Unknown")
179
+
180
+ report_sections.append(
181
+ f"- **{rec_type} Recommendation**: {impact} impact ({state})"
182
+ )
183
+
184
+ if "indexColumns" in details:
185
+ columns = ", ".join(details["indexColumns"])
186
+ report_sections.append(f" - **Columns**: {columns}")
187
+ else:
188
+ report_sections.append("✅ **No active performance recommendations**")
189
+ else:
190
+ report_sections.append("No performance recommendations available")
191
+
192
+ return "\n".join(report_sections)
193
+
194
+ def _invoke(self, params: Dict) -> StructuredToolResult:
195
+ try:
196
+ db_config = self.toolset.database_config()
197
+ client = self.toolset.api_client()
198
+
199
+ # Gather performance-related data
200
+ performance_data = self._gather_performance_data(db_config, client)
201
+
202
+ # Build the formatted report
203
+ report_text = self._build_performance_report(performance_data, db_config)
204
+
205
+ return StructuredToolResult(
206
+ status=ToolResultStatus.SUCCESS,
207
+ data=report_text,
208
+ params=params,
209
+ )
210
+ except Exception as e:
211
+ error_msg = f"Failed to generate performance report: {str(e)}"
212
+ logging.error(error_msg)
213
+ return StructuredToolResult(
214
+ status=ToolResultStatus.ERROR,
215
+ error=error_msg,
216
+ params=params,
217
+ )
218
+
219
+ def get_parameterized_one_liner(self, params: Dict) -> str:
220
+ db_config = self.toolset.database_config()
221
+ return f"Analyze performance for database {db_config.server_name}/{db_config.database_name}"
222
+
223
+ @staticmethod
224
+ def validate_config(
225
+ api_client: AzureSQLAPIClient, database_config: AzureSQLDatabaseConfig
226
+ ) -> Tuple[bool, str]:
227
+ errors = []
228
+
229
+ try:
230
+ # Test database advisors API access
231
+ api_client.get_database_advisors(
232
+ database_config.subscription_id,
233
+ database_config.resource_group,
234
+ database_config.server_name,
235
+ database_config.database_name,
236
+ )
237
+ except Exception as e:
238
+ error_msg = str(e)
239
+ if (
240
+ "authorization" in error_msg.lower()
241
+ or "permission" in error_msg.lower()
242
+ ):
243
+ errors.append(f"Database management API access denied: {error_msg}")
244
+ else:
245
+ errors.append(f"Database management API connection failed: {error_msg}")
246
+
247
+ if errors:
248
+ return False, "\n".join(errors)
249
+ return True, ""