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.
- holmes/.git_archival.json +7 -0
- holmes/__init__.py +76 -0
- holmes/__init__.py.bak +76 -0
- holmes/clients/robusta_client.py +24 -0
- holmes/common/env_vars.py +47 -0
- holmes/config.py +526 -0
- holmes/core/__init__.py +0 -0
- holmes/core/conversations.py +578 -0
- holmes/core/investigation.py +152 -0
- holmes/core/investigation_structured_output.py +264 -0
- holmes/core/issue.py +54 -0
- holmes/core/llm.py +250 -0
- holmes/core/models.py +157 -0
- holmes/core/openai_formatting.py +51 -0
- holmes/core/performance_timing.py +72 -0
- holmes/core/prompt.py +42 -0
- holmes/core/resource_instruction.py +17 -0
- holmes/core/runbooks.py +26 -0
- holmes/core/safeguards.py +120 -0
- holmes/core/supabase_dal.py +540 -0
- holmes/core/tool_calling_llm.py +798 -0
- holmes/core/tools.py +566 -0
- holmes/core/tools_utils/__init__.py +0 -0
- holmes/core/tools_utils/tool_executor.py +65 -0
- holmes/core/tools_utils/toolset_utils.py +52 -0
- holmes/core/toolset_manager.py +418 -0
- holmes/interactive.py +229 -0
- holmes/main.py +1041 -0
- holmes/plugins/__init__.py +0 -0
- holmes/plugins/destinations/__init__.py +6 -0
- holmes/plugins/destinations/slack/__init__.py +2 -0
- holmes/plugins/destinations/slack/plugin.py +163 -0
- holmes/plugins/interfaces.py +32 -0
- holmes/plugins/prompts/__init__.py +48 -0
- holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
- holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
- holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
- holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
- holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
- holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
- holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
- holmes/plugins/prompts/generic_ask.jinja2 +36 -0
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
- holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
- holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
- holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
- holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
- holmes/plugins/runbooks/README.md +22 -0
- holmes/plugins/runbooks/__init__.py +100 -0
- holmes/plugins/runbooks/catalog.json +14 -0
- holmes/plugins/runbooks/jira.yaml +12 -0
- holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
- holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
- holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
- holmes/plugins/sources/github/__init__.py +77 -0
- holmes/plugins/sources/jira/__init__.py +123 -0
- holmes/plugins/sources/opsgenie/__init__.py +93 -0
- holmes/plugins/sources/pagerduty/__init__.py +147 -0
- holmes/plugins/sources/prometheus/__init__.py +0 -0
- holmes/plugins/sources/prometheus/models.py +104 -0
- holmes/plugins/sources/prometheus/plugin.py +154 -0
- holmes/plugins/toolsets/__init__.py +171 -0
- holmes/plugins/toolsets/aks-node-health.yaml +65 -0
- holmes/plugins/toolsets/aks.yaml +86 -0
- holmes/plugins/toolsets/argocd.yaml +70 -0
- holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
- holmes/plugins/toolsets/aws.yaml +76 -0
- holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
- holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
- holmes/plugins/toolsets/azure_sql/install.md +66 -0
- holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
- holmes/plugins/toolsets/azure_sql/utils.py +83 -0
- holmes/plugins/toolsets/bash/__init__.py +0 -0
- holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
- holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
- holmes/plugins/toolsets/bash/common/bash.py +52 -0
- holmes/plugins/toolsets/bash/common/config.py +14 -0
- holmes/plugins/toolsets/bash/common/stringify.py +25 -0
- holmes/plugins/toolsets/bash/common/validators.py +24 -0
- holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
- holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
- holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
- holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
- holmes/plugins/toolsets/bash/parse_command.py +103 -0
- holmes/plugins/toolsets/confluence.yaml +19 -0
- holmes/plugins/toolsets/consts.py +5 -0
- holmes/plugins/toolsets/coralogix/api.py +158 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
- holmes/plugins/toolsets/coralogix/utils.py +181 -0
- holmes/plugins/toolsets/datadog.py +153 -0
- holmes/plugins/toolsets/docker.yaml +46 -0
- holmes/plugins/toolsets/git.py +756 -0
- holmes/plugins/toolsets/grafana/__init__.py +0 -0
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
- holmes/plugins/toolsets/grafana/common.py +68 -0
- holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
- holmes/plugins/toolsets/grafana/loki_api.py +89 -0
- holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
- holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
- holmes/plugins/toolsets/helm.yaml +42 -0
- holmes/plugins/toolsets/internet/internet.py +275 -0
- holmes/plugins/toolsets/internet/notion.py +137 -0
- holmes/plugins/toolsets/kafka.py +638 -0
- holmes/plugins/toolsets/kubernetes.yaml +255 -0
- holmes/plugins/toolsets/kubernetes_logs.py +426 -0
- holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
- holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
- holmes/plugins/toolsets/logging_utils/types.py +0 -0
- holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
- holmes/plugins/toolsets/newrelic.py +222 -0
- holmes/plugins/toolsets/opensearch/__init__.py +0 -0
- holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
- holmes/plugins/toolsets/rabbitmq/api.py +398 -0
- holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
- holmes/plugins/toolsets/robusta/__init__.py +0 -0
- holmes/plugins/toolsets/robusta/robusta.py +235 -0
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
- holmes/plugins/toolsets/runbook/__init__.py +0 -0
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
- holmes/plugins/toolsets/service_discovery.py +92 -0
- holmes/plugins/toolsets/servicenow/install.md +37 -0
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
- holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
- holmes/plugins/toolsets/slab.yaml +20 -0
- holmes/plugins/toolsets/utils.py +137 -0
- holmes/plugins/utils.py +14 -0
- holmes/utils/__init__.py +0 -0
- holmes/utils/cache.py +84 -0
- holmes/utils/cert_utils.py +40 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
- holmes/utils/definitions.py +13 -0
- holmes/utils/env.py +53 -0
- holmes/utils/file_utils.py +56 -0
- holmes/utils/global_instructions.py +20 -0
- holmes/utils/holmes_status.py +22 -0
- holmes/utils/holmes_sync_toolsets.py +80 -0
- holmes/utils/markdown_utils.py +55 -0
- holmes/utils/pydantic_utils.py +54 -0
- holmes/utils/robusta.py +10 -0
- holmes/utils/tags.py +97 -0
- holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
- holmesgpt-0.11.5.dist-info/METADATA +400 -0
- holmesgpt-0.11.5.dist-info/RECORD +183 -0
- holmesgpt-0.11.5.dist-info/WHEEL +4 -0
- 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, ""
|