holmesgpt 0.13.2__py3-none-any.whl → 0.18.4__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.
- holmes/__init__.py +3 -5
- holmes/clients/robusta_client.py +20 -6
- holmes/common/env_vars.py +58 -3
- holmes/common/openshift.py +1 -1
- holmes/config.py +123 -148
- holmes/core/conversations.py +71 -15
- holmes/core/feedback.py +191 -0
- holmes/core/investigation.py +31 -39
- holmes/core/investigation_structured_output.py +3 -3
- holmes/core/issue.py +1 -1
- holmes/core/llm.py +508 -88
- holmes/core/models.py +108 -4
- holmes/core/openai_formatting.py +14 -1
- holmes/core/prompt.py +48 -3
- holmes/core/runbooks.py +1 -0
- holmes/core/safeguards.py +8 -6
- holmes/core/supabase_dal.py +295 -100
- holmes/core/tool_calling_llm.py +489 -428
- holmes/core/tools.py +325 -56
- holmes/core/tools_utils/token_counting.py +21 -0
- holmes/core/tools_utils/tool_context_window_limiter.py +40 -0
- holmes/core/tools_utils/tool_executor.py +0 -13
- holmes/core/tools_utils/toolset_utils.py +1 -0
- holmes/core/toolset_manager.py +191 -5
- holmes/core/tracing.py +19 -3
- holmes/core/transformers/__init__.py +23 -0
- holmes/core/transformers/base.py +63 -0
- holmes/core/transformers/llm_summarize.py +175 -0
- holmes/core/transformers/registry.py +123 -0
- holmes/core/transformers/transformer.py +32 -0
- holmes/core/truncation/compaction.py +94 -0
- holmes/core/truncation/dal_truncation_utils.py +23 -0
- holmes/core/truncation/input_context_window_limiter.py +219 -0
- holmes/interactive.py +228 -31
- holmes/main.py +23 -40
- holmes/plugins/interfaces.py +2 -1
- holmes/plugins/prompts/__init__.py +2 -1
- holmes/plugins/prompts/_fetch_logs.jinja2 +31 -6
- holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
- holmes/plugins/prompts/_runbook_instructions.jinja2 +24 -12
- holmes/plugins/prompts/base_user_prompt.jinja2 +7 -0
- holmes/plugins/prompts/conversation_history_compaction.jinja2 +89 -0
- holmes/plugins/prompts/generic_ask.jinja2 +0 -4
- holmes/plugins/prompts/generic_ask_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +0 -1
- holmes/plugins/prompts/generic_investigation.jinja2 +0 -1
- holmes/plugins/prompts/investigation_procedure.jinja2 +50 -1
- holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +0 -1
- holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +0 -1
- holmes/plugins/runbooks/__init__.py +145 -17
- holmes/plugins/runbooks/catalog.json +2 -0
- holmes/plugins/sources/github/__init__.py +4 -2
- holmes/plugins/sources/prometheus/models.py +1 -0
- holmes/plugins/toolsets/__init__.py +44 -27
- holmes/plugins/toolsets/aks-node-health.yaml +46 -0
- holmes/plugins/toolsets/aks.yaml +64 -0
- holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +38 -47
- holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +2 -1
- holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +3 -2
- holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +3 -1
- holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +12 -13
- holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +11 -11
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +11 -9
- holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +15 -12
- holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +15 -15
- holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +11 -8
- holmes/plugins/toolsets/azure_sql/utils.py +0 -32
- holmes/plugins/toolsets/bash/argocd/__init__.py +3 -3
- holmes/plugins/toolsets/bash/aws/__init__.py +4 -4
- holmes/plugins/toolsets/bash/azure/__init__.py +4 -4
- holmes/plugins/toolsets/bash/bash_toolset.py +11 -15
- holmes/plugins/toolsets/bash/common/bash.py +23 -13
- holmes/plugins/toolsets/bash/common/bash_command.py +1 -1
- holmes/plugins/toolsets/bash/common/stringify.py +1 -1
- holmes/plugins/toolsets/bash/kubectl/__init__.py +2 -1
- holmes/plugins/toolsets/bash/kubectl/constants.py +0 -1
- holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +3 -4
- holmes/plugins/toolsets/bash/parse_command.py +12 -13
- holmes/plugins/toolsets/cilium.yaml +284 -0
- holmes/plugins/toolsets/connectivity_check.py +124 -0
- holmes/plugins/toolsets/coralogix/api.py +132 -119
- holmes/plugins/toolsets/coralogix/coralogix.jinja2 +14 -0
- holmes/plugins/toolsets/coralogix/toolset_coralogix.py +219 -0
- holmes/plugins/toolsets/coralogix/utils.py +15 -79
- holmes/plugins/toolsets/datadog/datadog_api.py +525 -26
- holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +55 -11
- holmes/plugins/toolsets/datadog/datadog_metrics_instructions.jinja2 +3 -3
- holmes/plugins/toolsets/datadog/datadog_models.py +59 -0
- holmes/plugins/toolsets/datadog/datadog_url_utils.py +213 -0
- holmes/plugins/toolsets/datadog/instructions_datadog_traces.jinja2 +165 -28
- holmes/plugins/toolsets/datadog/toolset_datadog_general.py +417 -241
- holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +234 -214
- holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +167 -79
- holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +374 -363
- holmes/plugins/toolsets/elasticsearch/__init__.py +6 -0
- holmes/plugins/toolsets/elasticsearch/elasticsearch.py +834 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_ppl_query_docs.jinja2 +1616 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist.py +78 -0
- holmes/plugins/toolsets/elasticsearch/opensearch_query_assist_instructions.jinja2 +223 -0
- holmes/plugins/toolsets/git.py +54 -50
- holmes/plugins/toolsets/grafana/base_grafana_toolset.py +16 -4
- holmes/plugins/toolsets/grafana/common.py +13 -29
- holmes/plugins/toolsets/grafana/grafana_tempo_api.py +455 -0
- holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +25 -0
- holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +191 -0
- holmes/plugins/toolsets/grafana/loki_api.py +4 -0
- holmes/plugins/toolsets/grafana/toolset_grafana.py +293 -89
- holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +49 -0
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
- holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +820 -292
- holmes/plugins/toolsets/grafana/trace_parser.py +4 -3
- holmes/plugins/toolsets/internet/internet.py +15 -16
- holmes/plugins/toolsets/internet/notion.py +9 -11
- holmes/plugins/toolsets/investigator/core_investigation.py +44 -36
- holmes/plugins/toolsets/investigator/model.py +3 -1
- holmes/plugins/toolsets/json_filter_mixin.py +134 -0
- holmes/plugins/toolsets/kafka.py +36 -42
- holmes/plugins/toolsets/kubernetes.yaml +317 -113
- holmes/plugins/toolsets/kubernetes_logs.py +9 -9
- holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
- holmes/plugins/toolsets/logging_utils/logging_api.py +94 -8
- holmes/plugins/toolsets/mcp/toolset_mcp.py +218 -64
- holmes/plugins/toolsets/newrelic/new_relic_api.py +165 -0
- holmes/plugins/toolsets/newrelic/newrelic.jinja2 +65 -0
- holmes/plugins/toolsets/newrelic/newrelic.py +320 -0
- holmes/plugins/toolsets/openshift.yaml +283 -0
- holmes/plugins/toolsets/prometheus/prometheus.py +1202 -421
- holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +54 -5
- holmes/plugins/toolsets/prometheus/utils.py +28 -0
- holmes/plugins/toolsets/rabbitmq/api.py +23 -4
- holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +13 -14
- holmes/plugins/toolsets/robusta/robusta.py +239 -68
- holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
- holmes/plugins/toolsets/runbook/runbook_fetcher.py +157 -27
- holmes/plugins/toolsets/service_discovery.py +1 -1
- holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
- holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
- holmes/plugins/toolsets/utils.py +88 -0
- holmes/utils/config_utils.py +91 -0
- holmes/utils/connection_utils.py +31 -0
- holmes/utils/console/result.py +10 -0
- holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
- holmes/utils/env.py +7 -0
- holmes/utils/file_utils.py +2 -1
- holmes/utils/global_instructions.py +60 -11
- holmes/utils/holmes_status.py +6 -4
- holmes/utils/holmes_sync_toolsets.py +0 -2
- holmes/utils/krr_utils.py +188 -0
- holmes/utils/log.py +15 -0
- holmes/utils/markdown_utils.py +2 -3
- holmes/utils/memory_limit.py +58 -0
- holmes/utils/sentry_helper.py +64 -0
- holmes/utils/stream.py +69 -8
- holmes/utils/tags.py +4 -3
- holmes/version.py +37 -15
- holmesgpt-0.18.4.dist-info/LICENSE +178 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/METADATA +35 -31
- holmesgpt-0.18.4.dist-info/RECORD +258 -0
- holmes/core/performance_timing.py +0 -72
- holmes/plugins/toolsets/aws.yaml +0 -80
- holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +0 -112
- holmes/plugins/toolsets/datadog/datadog_traces_formatter.py +0 -310
- holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +0 -739
- holmes/plugins/toolsets/grafana/grafana_api.py +0 -42
- holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
- holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
- holmes/plugins/toolsets/newrelic.py +0 -231
- holmes/plugins/toolsets/opensearch/opensearch.py +0 -257
- holmes/plugins/toolsets/opensearch/opensearch_logs.py +0 -161
- holmes/plugins/toolsets/opensearch/opensearch_traces.py +0 -218
- holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +0 -12
- holmes/plugins/toolsets/opensearch/opensearch_utils.py +0 -166
- holmes/plugins/toolsets/servicenow/install.md +0 -37
- holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
- holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
- holmes/utils/keygen_utils.py +0 -6
- holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
- holmesgpt-0.13.2.dist-info/RECORD +0 -234
- /holmes/plugins/toolsets/{opensearch → newrelic}/__init__.py +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/WHEEL +0 -0
- {holmesgpt-0.13.2.dist-info → holmesgpt-0.18.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Dict, Optional, Tuple
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
from holmes.core.tools import (
|
|
7
|
+
StructuredToolResult,
|
|
8
|
+
StructuredToolResultStatus,
|
|
9
|
+
Tool,
|
|
10
|
+
ToolInvokeContext,
|
|
11
|
+
ToolParameter,
|
|
12
|
+
)
|
|
13
|
+
from holmes.plugins.toolsets.consts import (
|
|
14
|
+
STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
|
|
15
|
+
)
|
|
16
|
+
from holmes.plugins.toolsets.grafana.common import GrafanaConfig, get_base_url
|
|
17
|
+
from holmes.plugins.toolsets.grafana.loki_api import (
|
|
18
|
+
execute_loki_query,
|
|
19
|
+
)
|
|
20
|
+
from holmes.plugins.toolsets.grafana.toolset_grafana import BaseGrafanaToolset
|
|
21
|
+
from holmes.plugins.toolsets.logging_utils.logging_api import (
|
|
22
|
+
DEFAULT_LOG_LIMIT,
|
|
23
|
+
DEFAULT_TIME_SPAN_SECONDS,
|
|
24
|
+
)
|
|
25
|
+
from holmes.plugins.toolsets.utils import (
|
|
26
|
+
process_timestamps_to_rfc3339,
|
|
27
|
+
standard_start_datetime_tool_param_description,
|
|
28
|
+
toolset_name_for_one_liner,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _build_grafana_loki_explore_url(
|
|
33
|
+
config: GrafanaConfig, query: str, start: str, end: str, limit: int = 100
|
|
34
|
+
) -> Optional[str]:
|
|
35
|
+
if not config.grafana_datasource_uid:
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
base_url = config.external_url or config.url
|
|
39
|
+
datasource_uid = config.grafana_datasource_uid or "loki"
|
|
40
|
+
|
|
41
|
+
from_str = start if start else "now-1h"
|
|
42
|
+
to_str = end if end else "now"
|
|
43
|
+
|
|
44
|
+
pane_id = "tmp"
|
|
45
|
+
safe_query = query if query else "{}"
|
|
46
|
+
panes = {
|
|
47
|
+
pane_id: {
|
|
48
|
+
"datasource": datasource_uid,
|
|
49
|
+
"queries": [
|
|
50
|
+
{
|
|
51
|
+
"refId": "A",
|
|
52
|
+
"datasource": {"type": "loki", "uid": datasource_uid},
|
|
53
|
+
"expr": safe_query,
|
|
54
|
+
"queryType": "range",
|
|
55
|
+
"maxLines": limit,
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"range": {"from": from_str, "to": to_str},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
panes_encoded = quote(
|
|
63
|
+
json.dumps(panes, separators=(",", ":"), ensure_ascii=False), safe=""
|
|
64
|
+
)
|
|
65
|
+
return f"{base_url}/explore?schemaVersion=1&panes={panes_encoded}&orgId=1"
|
|
66
|
+
except Exception:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class GrafanaLokiToolset(BaseGrafanaToolset):
|
|
71
|
+
def health_check(self) -> Tuple[bool, str]:
|
|
72
|
+
"""Test a dummy query to check if service available."""
|
|
73
|
+
(start, end) = process_timestamps_to_rfc3339(
|
|
74
|
+
start_timestamp=-1,
|
|
75
|
+
end_timestamp=None,
|
|
76
|
+
default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
c = self._grafana_config
|
|
80
|
+
try:
|
|
81
|
+
_ = execute_loki_query(
|
|
82
|
+
base_url=get_base_url(c),
|
|
83
|
+
api_key=c.api_key,
|
|
84
|
+
headers=c.headers,
|
|
85
|
+
query='{job="test_endpoint"}',
|
|
86
|
+
start=start,
|
|
87
|
+
end=end,
|
|
88
|
+
limit=1,
|
|
89
|
+
verify_ssl=c.verify_ssl,
|
|
90
|
+
)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return False, f"Unable to connect to Loki.\n{str(e)}"
|
|
93
|
+
return True, ""
|
|
94
|
+
|
|
95
|
+
def __init__(self):
|
|
96
|
+
super().__init__(
|
|
97
|
+
name="grafana/loki",
|
|
98
|
+
description="Runs loki log queries using Grafana Loki or Loki directly.",
|
|
99
|
+
icon_url="https://grafana.com/media/docs/loki/logo-grafana-loki.png",
|
|
100
|
+
docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanaloki/",
|
|
101
|
+
tools=[],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
self.tools = [LokiQuery(toolset=self)]
|
|
105
|
+
instructions_filepath = os.path.abspath(
|
|
106
|
+
os.path.join(os.path.dirname(__file__), "instructions.jinja2")
|
|
107
|
+
)
|
|
108
|
+
self._load_llm_instructions(jinja_template=f"file://{instructions_filepath}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class LokiQuery(Tool):
|
|
112
|
+
toolset: GrafanaLokiToolset
|
|
113
|
+
name: str = "grafana_loki_query"
|
|
114
|
+
description: str = "Run a query against Grafana Loki using LogQL query language."
|
|
115
|
+
parameters: Dict[str, ToolParameter] = {
|
|
116
|
+
"query": ToolParameter(
|
|
117
|
+
description="LogQL query string.",
|
|
118
|
+
type="string",
|
|
119
|
+
required=True,
|
|
120
|
+
),
|
|
121
|
+
"start": ToolParameter(
|
|
122
|
+
description=standard_start_datetime_tool_param_description(
|
|
123
|
+
DEFAULT_TIME_SPAN_SECONDS
|
|
124
|
+
),
|
|
125
|
+
type="string",
|
|
126
|
+
required=False,
|
|
127
|
+
),
|
|
128
|
+
"end": ToolParameter(
|
|
129
|
+
description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
|
|
130
|
+
type="string",
|
|
131
|
+
required=False,
|
|
132
|
+
),
|
|
133
|
+
"limit": ToolParameter(
|
|
134
|
+
description=f"Maximum number of entries to return (default: {DEFAULT_LOG_LIMIT})",
|
|
135
|
+
type="integer",
|
|
136
|
+
required=False,
|
|
137
|
+
),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def get_parameterized_one_liner(self, params) -> str:
|
|
141
|
+
return f"{toolset_name_for_one_liner(self.toolset.name)}: loki query {params}"
|
|
142
|
+
|
|
143
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
144
|
+
(start, end) = process_timestamps_to_rfc3339(
|
|
145
|
+
start_timestamp=params.get("start"),
|
|
146
|
+
end_timestamp=params.get("end"),
|
|
147
|
+
default_time_span_seconds=DEFAULT_TIME_SPAN_SECONDS,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
config = self.toolset._grafana_config
|
|
151
|
+
query_str = params.get("query", '{query="no_query_fallback"}')
|
|
152
|
+
try:
|
|
153
|
+
data = execute_loki_query(
|
|
154
|
+
base_url=get_base_url(config),
|
|
155
|
+
api_key=config.api_key,
|
|
156
|
+
headers=config.headers,
|
|
157
|
+
query=query_str,
|
|
158
|
+
start=start,
|
|
159
|
+
end=end,
|
|
160
|
+
limit=params.get("limit") or DEFAULT_LOG_LIMIT,
|
|
161
|
+
verify_ssl=config.verify_ssl,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
explore_url = _build_grafana_loki_explore_url(
|
|
165
|
+
config,
|
|
166
|
+
query_str,
|
|
167
|
+
start,
|
|
168
|
+
end,
|
|
169
|
+
limit=params.get("limit") or DEFAULT_LOG_LIMIT,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if data:
|
|
173
|
+
return StructuredToolResult(
|
|
174
|
+
status=StructuredToolResultStatus.SUCCESS,
|
|
175
|
+
data=data,
|
|
176
|
+
params=params,
|
|
177
|
+
url=explore_url,
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
return StructuredToolResult(
|
|
181
|
+
status=StructuredToolResultStatus.NO_DATA,
|
|
182
|
+
params=params,
|
|
183
|
+
url=explore_url,
|
|
184
|
+
)
|
|
185
|
+
except Exception as e:
|
|
186
|
+
return StructuredToolResult(
|
|
187
|
+
status=StructuredToolResultStatus.ERROR,
|
|
188
|
+
params=params,
|
|
189
|
+
error=str(e),
|
|
190
|
+
url=f"{get_base_url(config)}/loki/api/v1/query_range",
|
|
191
|
+
)
|
|
@@ -42,6 +42,7 @@ def execute_loki_query(
|
|
|
42
42
|
start: Union[int, str],
|
|
43
43
|
end: Union[int, str],
|
|
44
44
|
limit: int,
|
|
45
|
+
verify_ssl: bool = True,
|
|
45
46
|
) -> List[Dict]:
|
|
46
47
|
params = {"query": query, "limit": limit, "start": start, "end": end}
|
|
47
48
|
try:
|
|
@@ -50,6 +51,7 @@ def execute_loki_query(
|
|
|
50
51
|
url,
|
|
51
52
|
headers=build_headers(api_key=api_key, additional_headers=headers),
|
|
52
53
|
params=params, # type: ignore
|
|
54
|
+
verify=verify_ssl,
|
|
53
55
|
)
|
|
54
56
|
response.raise_for_status()
|
|
55
57
|
|
|
@@ -74,6 +76,7 @@ def query_loki_logs_by_label(
|
|
|
74
76
|
label: str,
|
|
75
77
|
namespace_search_key: str = "namespace",
|
|
76
78
|
limit: int = 200,
|
|
79
|
+
verify_ssl: bool = True,
|
|
77
80
|
) -> List[Dict]:
|
|
78
81
|
query = f'{{{namespace_search_key}="{namespace}", {label}="{label_value}"}}'
|
|
79
82
|
if filter:
|
|
@@ -86,4 +89,5 @@ def query_loki_logs_by_label(
|
|
|
86
89
|
start=start,
|
|
87
90
|
end=end,
|
|
88
91
|
limit=limit,
|
|
92
|
+
verify_ssl=verify_ssl,
|
|
89
93
|
)
|
|
@@ -1,127 +1,331 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from typing import Any, ClassVar, Dict, Optional, Tuple, Type, cast
|
|
2
4
|
from urllib.parse import urlencode, urljoin
|
|
5
|
+
|
|
6
|
+
import requests # type: ignore
|
|
7
|
+
|
|
3
8
|
from holmes.core.tools import (
|
|
4
9
|
StructuredToolResult,
|
|
10
|
+
StructuredToolResultStatus,
|
|
5
11
|
Tool,
|
|
12
|
+
ToolInvokeContext,
|
|
6
13
|
ToolParameter,
|
|
7
|
-
ToolResultStatus,
|
|
8
14
|
)
|
|
9
15
|
from holmes.plugins.toolsets.grafana.base_grafana_toolset import BaseGrafanaToolset
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
from holmes.plugins.toolsets.grafana.common import (
|
|
17
|
+
GrafanaConfig,
|
|
18
|
+
build_headers,
|
|
19
|
+
get_base_url,
|
|
20
|
+
)
|
|
21
|
+
from holmes.plugins.toolsets.json_filter_mixin import JsonFilterMixin
|
|
13
22
|
from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
|
|
14
23
|
|
|
15
24
|
|
|
16
|
-
class
|
|
17
|
-
|
|
25
|
+
class GrafanaDashboardConfig(GrafanaConfig):
|
|
26
|
+
"""Configuration specific to Grafana Dashboard toolset."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_grafana_dashboard_url(
|
|
32
|
+
config: GrafanaDashboardConfig,
|
|
33
|
+
uid: Optional[str] = None,
|
|
34
|
+
query_params: Optional[Dict[str, Any]] = None,
|
|
35
|
+
) -> Optional[str]:
|
|
36
|
+
try:
|
|
37
|
+
base_url = config.external_url or config.url
|
|
38
|
+
if uid:
|
|
39
|
+
return f"{base_url.rstrip('/')}/d/{uid}"
|
|
40
|
+
else:
|
|
41
|
+
query_string = urlencode(query_params, doseq=True) if query_params else ""
|
|
42
|
+
if query_string:
|
|
43
|
+
return f"{base_url.rstrip('/')}/dashboards?{query_string}"
|
|
44
|
+
else:
|
|
45
|
+
return f"{base_url.rstrip('/')}/dashboards"
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GrafanaToolset(BaseGrafanaToolset):
|
|
51
|
+
config_class: ClassVar[Type[GrafanaDashboardConfig]] = GrafanaDashboardConfig
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
18
54
|
super().__init__(
|
|
19
|
-
name="
|
|
20
|
-
description="
|
|
55
|
+
name="grafana/dashboards",
|
|
56
|
+
description="Provides tools for interacting with Grafana dashboards",
|
|
57
|
+
icon_url="https://w7.pngwing.com/pngs/434/923/png-transparent-grafana-hd-logo-thumbnail.png",
|
|
58
|
+
docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanadashboards/",
|
|
59
|
+
tools=[
|
|
60
|
+
SearchDashboards(self),
|
|
61
|
+
GetDashboardByUID(self),
|
|
62
|
+
GetHomeDashboard(self),
|
|
63
|
+
GetDashboardTags(self),
|
|
64
|
+
],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self._load_llm_instructions_from_file(
|
|
68
|
+
os.path.dirname(__file__), "toolset_grafana_dashboard.jinja2"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def health_check(self) -> Tuple[bool, str]:
|
|
72
|
+
"""Test connectivity by invoking GetDashboardTags tool."""
|
|
73
|
+
tool = GetDashboardTags(self)
|
|
74
|
+
try:
|
|
75
|
+
_ = tool._make_grafana_request("api/dashboards/tags", {})
|
|
76
|
+
return True, ""
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return False, f"Failed to connect to Grafana {str(e)}"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def grafana_config(self) -> GrafanaDashboardConfig:
|
|
82
|
+
return cast(GrafanaDashboardConfig, self._grafana_config)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class BaseGrafanaTool(Tool, ABC):
|
|
86
|
+
"""Base class for Grafana tools with common HTTP request functionality."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, toolset: GrafanaToolset, *args, **kwargs):
|
|
89
|
+
super().__init__(*args, **kwargs)
|
|
90
|
+
self._toolset = toolset
|
|
91
|
+
|
|
92
|
+
def _make_grafana_request(
|
|
93
|
+
self,
|
|
94
|
+
endpoint: str,
|
|
95
|
+
params: dict,
|
|
96
|
+
query_params: Optional[Dict] = None,
|
|
97
|
+
timeout: int = 30,
|
|
98
|
+
) -> StructuredToolResult:
|
|
99
|
+
"""Make a GET request to Grafana API and return structured result.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
endpoint: API endpoint path (e.g., "/api/search")
|
|
103
|
+
params: Original parameters passed to the tool
|
|
104
|
+
query_params: Optional query parameters for the request
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
StructuredToolResult with the API response data
|
|
108
|
+
"""
|
|
109
|
+
base_url = get_base_url(self._toolset.grafana_config)
|
|
110
|
+
if not base_url.endswith("/"):
|
|
111
|
+
base_url += "/"
|
|
112
|
+
url = urljoin(base_url, endpoint)
|
|
113
|
+
headers = build_headers(
|
|
114
|
+
api_key=self._toolset.grafana_config.api_key,
|
|
115
|
+
additional_headers=self._toolset.grafana_config.headers,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
response = requests.get(
|
|
119
|
+
url,
|
|
120
|
+
headers=headers,
|
|
121
|
+
params=query_params,
|
|
122
|
+
timeout=timeout,
|
|
123
|
+
verify=self._toolset.grafana_config.verify_ssl,
|
|
124
|
+
)
|
|
125
|
+
response.raise_for_status()
|
|
126
|
+
data = response.json()
|
|
127
|
+
|
|
128
|
+
return StructuredToolResult(
|
|
129
|
+
status=StructuredToolResultStatus.SUCCESS,
|
|
130
|
+
data=data,
|
|
131
|
+
url=url,
|
|
132
|
+
params=params,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SearchDashboards(BaseGrafanaTool):
|
|
137
|
+
def __init__(self, toolset: GrafanaToolset):
|
|
138
|
+
super().__init__(
|
|
139
|
+
toolset=toolset,
|
|
140
|
+
name="grafana_search_dashboards",
|
|
141
|
+
description="Search for Grafana dashboards and folders using the /api/search endpoint",
|
|
21
142
|
parameters={
|
|
22
|
-
"
|
|
23
|
-
description="
|
|
143
|
+
"query": ToolParameter(
|
|
144
|
+
description="Search text to filter dashboards",
|
|
145
|
+
type="string",
|
|
146
|
+
required=False,
|
|
147
|
+
),
|
|
148
|
+
"tag": ToolParameter(
|
|
149
|
+
description="Search dashboards by tag",
|
|
24
150
|
type="string",
|
|
25
151
|
required=False,
|
|
26
152
|
),
|
|
27
|
-
"
|
|
28
|
-
description="
|
|
153
|
+
"type": ToolParameter(
|
|
154
|
+
description="Filter by type: 'dash-folder' or 'dash-db'",
|
|
29
155
|
type="string",
|
|
30
156
|
required=False,
|
|
31
157
|
),
|
|
32
|
-
"
|
|
33
|
-
description="
|
|
158
|
+
"dashboardIds": ToolParameter(
|
|
159
|
+
description="List of dashboard IDs to filter (comma-separated)",
|
|
34
160
|
type="string",
|
|
35
161
|
required=False,
|
|
36
162
|
),
|
|
37
|
-
"
|
|
38
|
-
description="
|
|
163
|
+
"dashboardUIDs": ToolParameter(
|
|
164
|
+
description="List of dashboard UIDs to search for (comma-separated)",
|
|
39
165
|
type="string",
|
|
40
166
|
required=False,
|
|
41
167
|
),
|
|
168
|
+
"folderUIDs": ToolParameter(
|
|
169
|
+
description="List of folder UIDs to search within (comma-separated)",
|
|
170
|
+
type="string",
|
|
171
|
+
required=False,
|
|
172
|
+
),
|
|
173
|
+
"starred": ToolParameter(
|
|
174
|
+
description="Return only starred dashboards",
|
|
175
|
+
type="boolean",
|
|
176
|
+
required=False,
|
|
177
|
+
),
|
|
178
|
+
"limit": ToolParameter(
|
|
179
|
+
description="Maximum results (default 1000, max 5000)",
|
|
180
|
+
type="integer",
|
|
181
|
+
required=False,
|
|
182
|
+
),
|
|
183
|
+
"page": ToolParameter(
|
|
184
|
+
description="Page number for pagination",
|
|
185
|
+
type="integer",
|
|
186
|
+
required=False,
|
|
187
|
+
),
|
|
42
188
|
},
|
|
43
189
|
)
|
|
44
|
-
self._toolset = toolset
|
|
45
190
|
|
|
46
|
-
def _invoke(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
191
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
192
|
+
query_params = {}
|
|
193
|
+
if params.get("query"):
|
|
194
|
+
query_params["query"] = params["query"]
|
|
195
|
+
if params.get("tag"):
|
|
196
|
+
query_params["tag"] = params["tag"]
|
|
197
|
+
if params.get("type"):
|
|
198
|
+
query_params["type"] = params["type"]
|
|
199
|
+
if params.get("dashboardIds"):
|
|
200
|
+
# Check if dashboardIds also needs to be passed as multiple params
|
|
201
|
+
dashboard_ids = params["dashboardIds"].split(",")
|
|
202
|
+
query_params["dashboardIds"] = [
|
|
203
|
+
dashboard_id.strip()
|
|
204
|
+
for dashboard_id in dashboard_ids
|
|
205
|
+
if dashboard_id.strip()
|
|
206
|
+
]
|
|
207
|
+
if params.get("dashboardUIDs"):
|
|
208
|
+
# Handle dashboardUIDs as a list - split comma-separated values
|
|
209
|
+
dashboard_uids = params["dashboardUIDs"].split(",")
|
|
210
|
+
query_params["dashboardUIDs"] = [
|
|
211
|
+
uid.strip() for uid in dashboard_uids if uid.strip()
|
|
212
|
+
]
|
|
213
|
+
if params.get("folderUIDs"):
|
|
214
|
+
# Check if folderUIDs also needs to be passed as multiple params
|
|
215
|
+
folder_uids = params["folderUIDs"].split(",")
|
|
216
|
+
query_params["folderUIDs"] = [
|
|
217
|
+
uid.strip() for uid in folder_uids if uid.strip()
|
|
218
|
+
]
|
|
219
|
+
if params.get("starred") is not None:
|
|
220
|
+
query_params["starred"] = str(params["starred"]).lower()
|
|
221
|
+
if params.get("limit"):
|
|
222
|
+
query_params["limit"] = params["limit"]
|
|
223
|
+
if params.get("page"):
|
|
224
|
+
query_params["page"] = params["page"]
|
|
225
|
+
|
|
226
|
+
result = self._make_grafana_request("api/search", params, query_params)
|
|
227
|
+
|
|
228
|
+
config = self._toolset.grafana_config
|
|
229
|
+
search_url = _build_grafana_dashboard_url(config, query_params=query_params)
|
|
230
|
+
|
|
231
|
+
if params.get("dashboardUIDs"):
|
|
232
|
+
uids = [
|
|
233
|
+
uid.strip() for uid in params["dashboardUIDs"].split(",") if uid.strip()
|
|
234
|
+
]
|
|
235
|
+
if len(uids) == 1:
|
|
236
|
+
search_url = _build_grafana_dashboard_url(config, uid=uids[0])
|
|
237
|
+
|
|
238
|
+
return StructuredToolResult(
|
|
239
|
+
status=result.status,
|
|
240
|
+
data=result.data,
|
|
241
|
+
params=result.params,
|
|
242
|
+
url=search_url if search_url else None,
|
|
51
243
|
)
|
|
52
|
-
headers = {"Authorization": f"Bearer {self._toolset._grafana_config.api_key}"}
|
|
53
244
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"var-namespace": params.get("namespace", ""),
|
|
72
|
-
"var-pod": params.get("pod_name", ""),
|
|
73
|
-
"var-node": params.get("node_name", ""),
|
|
74
|
-
"var-datasource": self._toolset._grafana_config.grafana_datasource_uid,
|
|
75
|
-
"refresh": "5s",
|
|
245
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
246
|
+
return f"{toolset_name_for_one_liner(self._toolset.name)}: Search Dashboards"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class GetDashboardByUID(JsonFilterMixin, BaseGrafanaTool):
|
|
250
|
+
def __init__(self, toolset: GrafanaToolset):
|
|
251
|
+
super().__init__(
|
|
252
|
+
toolset=toolset,
|
|
253
|
+
name="grafana_get_dashboard_by_uid",
|
|
254
|
+
description="Get a dashboard by its UID using the /api/dashboards/uid/:uid endpoint",
|
|
255
|
+
parameters=self.extend_parameters(
|
|
256
|
+
{
|
|
257
|
+
"uid": ToolParameter(
|
|
258
|
+
description="The unique identifier of the dashboard",
|
|
259
|
+
type="string",
|
|
260
|
+
required=True,
|
|
261
|
+
)
|
|
76
262
|
}
|
|
263
|
+
),
|
|
264
|
+
)
|
|
77
265
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
f"Title: {dash['title']}\nURL: {dashboard_url}\n"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
return StructuredToolResult(
|
|
93
|
-
status=ToolResultStatus.SUCCESS
|
|
94
|
-
if formatted_dashboards
|
|
95
|
-
else ToolResultStatus.NO_DATA,
|
|
96
|
-
data="\n".join(formatted_dashboards)
|
|
97
|
-
if formatted_dashboards
|
|
98
|
-
else "No dashboards found.",
|
|
99
|
-
url=url,
|
|
100
|
-
params=params,
|
|
101
|
-
)
|
|
102
|
-
except requests.RequestException as e:
|
|
103
|
-
logging.error(f"Error fetching dashboards: {str(e)}")
|
|
104
|
-
return StructuredToolResult(
|
|
105
|
-
status=ToolResultStatus.ERROR,
|
|
106
|
-
error=f"Error fetching dashboards: {str(e)}",
|
|
107
|
-
url=url,
|
|
108
|
-
params=params,
|
|
109
|
-
)
|
|
266
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
267
|
+
uid = params["uid"]
|
|
268
|
+
result = self._make_grafana_request(f"api/dashboards/uid/{uid}", params)
|
|
269
|
+
|
|
270
|
+
dashboard_url = _build_grafana_dashboard_url(
|
|
271
|
+
self._toolset.grafana_config, uid=uid
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
filtered_result = self.filter_result(result, params)
|
|
275
|
+
filtered_result.url = dashboard_url if dashboard_url else result.url
|
|
276
|
+
return filtered_result
|
|
110
277
|
|
|
111
278
|
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
112
|
-
return (
|
|
113
|
-
|
|
279
|
+
return f"{toolset_name_for_one_liner(self._toolset.name)}: Get Dashboard {params.get('uid', '')}"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class GetHomeDashboard(JsonFilterMixin, BaseGrafanaTool):
|
|
283
|
+
def __init__(self, toolset: GrafanaToolset):
|
|
284
|
+
super().__init__(
|
|
285
|
+
toolset=toolset,
|
|
286
|
+
name="grafana_get_home_dashboard",
|
|
287
|
+
description="Get the home dashboard using the /api/dashboards/home endpoint",
|
|
288
|
+
parameters=self.extend_parameters({}),
|
|
114
289
|
)
|
|
115
290
|
|
|
291
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
292
|
+
result = self._make_grafana_request("api/dashboards/home", params)
|
|
293
|
+
config = self._toolset.grafana_config
|
|
294
|
+
dashboard_url = None
|
|
295
|
+
if isinstance(result.data, dict):
|
|
296
|
+
uid = result.data.get("dashboard", {}).get("uid")
|
|
297
|
+
if uid:
|
|
298
|
+
dashboard_url = _build_grafana_dashboard_url(config, uid=uid)
|
|
116
299
|
|
|
117
|
-
|
|
118
|
-
|
|
300
|
+
filtered_result = self.filter_result(result, params)
|
|
301
|
+
filtered_result.url = dashboard_url if dashboard_url else None
|
|
302
|
+
return filtered_result
|
|
303
|
+
|
|
304
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
305
|
+
return f"{toolset_name_for_one_liner(self._toolset.name)}: Get Home Dashboard"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class GetDashboardTags(BaseGrafanaTool):
|
|
309
|
+
def __init__(self, toolset: GrafanaToolset):
|
|
119
310
|
super().__init__(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
311
|
+
toolset=toolset,
|
|
312
|
+
name="grafana_get_dashboard_tags",
|
|
313
|
+
description="Get all tags used across dashboards using the /api/dashboards/tags endpoint",
|
|
314
|
+
parameters={},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
|
|
318
|
+
result = self._make_grafana_request("api/dashboards/tags", params)
|
|
319
|
+
|
|
320
|
+
config = self._toolset.grafana_config
|
|
321
|
+
tags_url = _build_grafana_dashboard_url(config)
|
|
322
|
+
|
|
323
|
+
return StructuredToolResult(
|
|
324
|
+
status=result.status,
|
|
325
|
+
data=result.data,
|
|
326
|
+
params=result.params,
|
|
327
|
+
url=tags_url,
|
|
127
328
|
)
|
|
329
|
+
|
|
330
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
331
|
+
return f"{toolset_name_for_one_liner(self._toolset.name)}: Get Dashboard Tags"
|