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,299 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, List, cast
|
|
3
|
+
|
|
4
|
+
import requests # type: ignore
|
|
5
|
+
import yaml # type: ignore
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from holmes.common.env_vars import load_bool
|
|
9
|
+
from holmes.core.tools import (
|
|
10
|
+
StructuredToolResult,
|
|
11
|
+
Tool,
|
|
12
|
+
ToolParameter,
|
|
13
|
+
ToolResultStatus,
|
|
14
|
+
)
|
|
15
|
+
from holmes.plugins.toolsets.grafana.base_grafana_toolset import BaseGrafanaToolset
|
|
16
|
+
from holmes.plugins.toolsets.grafana.common import (
|
|
17
|
+
GrafanaConfig,
|
|
18
|
+
build_headers,
|
|
19
|
+
get_base_url,
|
|
20
|
+
)
|
|
21
|
+
from holmes.plugins.toolsets.grafana.tempo_api import (
|
|
22
|
+
query_tempo_trace_by_id,
|
|
23
|
+
query_tempo_traces,
|
|
24
|
+
)
|
|
25
|
+
from holmes.plugins.toolsets.grafana.trace_parser import format_traces_list
|
|
26
|
+
from holmes.plugins.toolsets.utils import get_param_or_raise, process_timestamps_to_int
|
|
27
|
+
|
|
28
|
+
TEMPO_LABELS_ADD_PREFIX = load_bool("TEMPO_LABELS_ADD_PREFIX", True)
|
|
29
|
+
|
|
30
|
+
ONE_HOUR_IN_SECONDS = 3600
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GrafanaTempoLabelsConfig(BaseModel):
|
|
34
|
+
pod: str = "k8s.pod.name"
|
|
35
|
+
namespace: str = "k8s.namespace.name"
|
|
36
|
+
deployment: str = "k8s.deployment.name"
|
|
37
|
+
node: str = "k8s.node.name"
|
|
38
|
+
service: str = "service.name"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GrafanaTempoConfig(GrafanaConfig):
|
|
42
|
+
labels: GrafanaTempoLabelsConfig = GrafanaTempoLabelsConfig()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BaseGrafanaTempoToolset(BaseGrafanaToolset):
|
|
46
|
+
config_class = GrafanaTempoConfig
|
|
47
|
+
|
|
48
|
+
def get_example_config(self):
|
|
49
|
+
example_config = GrafanaTempoConfig(
|
|
50
|
+
api_key="YOUR API KEY",
|
|
51
|
+
url="YOUR GRAFANA URL",
|
|
52
|
+
grafana_datasource_uid="<UID of the tempo datasource to use>",
|
|
53
|
+
)
|
|
54
|
+
return example_config.model_dump()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def grafana_config(self) -> GrafanaTempoConfig:
|
|
58
|
+
return cast(GrafanaTempoConfig, self._grafana_config)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_params(params: Dict[str, Any], expected_params: List[str]):
|
|
62
|
+
for param in expected_params:
|
|
63
|
+
if param in params and params[param] not in (None, "", [], {}):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return f"At least one of the following argument is expected but none were set: {expected_params}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class GetTempoTraces(Tool):
|
|
70
|
+
def __init__(self, toolset: BaseGrafanaTempoToolset):
|
|
71
|
+
super().__init__(
|
|
72
|
+
name="fetch_tempo_traces",
|
|
73
|
+
description="""Lists Tempo traces. At least one of `service_name`, `pod_name` or `deployment_name` argument is required.""",
|
|
74
|
+
parameters={
|
|
75
|
+
"min_duration": ToolParameter(
|
|
76
|
+
description="The minimum duration of traces to fetch, e.g., '5s' for 5 seconds.",
|
|
77
|
+
type="string",
|
|
78
|
+
required=True,
|
|
79
|
+
),
|
|
80
|
+
"service_name": ToolParameter(
|
|
81
|
+
description="Filter traces by service name",
|
|
82
|
+
type="string",
|
|
83
|
+
required=False,
|
|
84
|
+
),
|
|
85
|
+
"pod_name": ToolParameter(
|
|
86
|
+
description="Filter traces by pod name",
|
|
87
|
+
type="string",
|
|
88
|
+
required=False,
|
|
89
|
+
),
|
|
90
|
+
"namespace_name": ToolParameter(
|
|
91
|
+
description="Filter traces by namespace",
|
|
92
|
+
type="string",
|
|
93
|
+
required=False,
|
|
94
|
+
),
|
|
95
|
+
"deployment_name": ToolParameter(
|
|
96
|
+
description="Filter traces by deployment name",
|
|
97
|
+
type="string",
|
|
98
|
+
required=False,
|
|
99
|
+
),
|
|
100
|
+
"node_name": ToolParameter(
|
|
101
|
+
description="Filter traces by node",
|
|
102
|
+
type="string",
|
|
103
|
+
required=False,
|
|
104
|
+
),
|
|
105
|
+
"start_datetime": ToolParameter(
|
|
106
|
+
description="The beginning time boundary for the trace search period. String in RFC3339 format. If a negative integer, the number of seconds relative to the end_timestamp. Defaults to negative one hour (-3600)",
|
|
107
|
+
type="string",
|
|
108
|
+
required=False,
|
|
109
|
+
),
|
|
110
|
+
"end_datetime": ToolParameter(
|
|
111
|
+
description="The ending time boundary for the trace search period. String in RFC3339 format. Defaults to NOW().",
|
|
112
|
+
type="string",
|
|
113
|
+
required=False,
|
|
114
|
+
),
|
|
115
|
+
"limit": ToolParameter(
|
|
116
|
+
description="Maximum number of traces to return. Defaults to 50",
|
|
117
|
+
type="string",
|
|
118
|
+
required=False,
|
|
119
|
+
),
|
|
120
|
+
"sort": ToolParameter(
|
|
121
|
+
description="One of 'descending', 'ascending' or 'none' for no sorting. Defaults to descending",
|
|
122
|
+
type="string",
|
|
123
|
+
required=False,
|
|
124
|
+
),
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
self._toolset = toolset
|
|
128
|
+
|
|
129
|
+
def _invoke(self, params: Dict) -> StructuredToolResult:
|
|
130
|
+
api_key = self._toolset.grafana_config.api_key
|
|
131
|
+
headers = self._toolset.grafana_config.headers
|
|
132
|
+
labels = self._toolset.grafana_config.labels
|
|
133
|
+
|
|
134
|
+
invalid_params_error = validate_params(
|
|
135
|
+
params, ["service_name", "pod_name", "deployment_name"]
|
|
136
|
+
)
|
|
137
|
+
if invalid_params_error:
|
|
138
|
+
return StructuredToolResult(
|
|
139
|
+
status=ToolResultStatus.ERROR,
|
|
140
|
+
error=invalid_params_error,
|
|
141
|
+
params=params,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
start, end = process_timestamps_to_int(
|
|
145
|
+
params.get("start_datetime"),
|
|
146
|
+
params.get("end_datetime"),
|
|
147
|
+
default_time_span_seconds=3600,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
prefix = ""
|
|
151
|
+
if TEMPO_LABELS_ADD_PREFIX:
|
|
152
|
+
prefix = "resource."
|
|
153
|
+
|
|
154
|
+
filters = []
|
|
155
|
+
if params.get("service_name"):
|
|
156
|
+
filters.append(f'{prefix}{labels.service}="{params.get("service_name")}"')
|
|
157
|
+
if params.get("pod_name"):
|
|
158
|
+
filters.append(f'{prefix}{labels.pod}="{params.get("pod_name")}"')
|
|
159
|
+
if params.get("namespace_name"):
|
|
160
|
+
filters.append(
|
|
161
|
+
f'{prefix}{labels.namespace}="{params.get("namespace_name")}"'
|
|
162
|
+
)
|
|
163
|
+
if params.get("deployment_name"):
|
|
164
|
+
filters.append(
|
|
165
|
+
f'{prefix}{labels.deployment}="{params.get("deployment_name")}"'
|
|
166
|
+
)
|
|
167
|
+
if params.get("node_name"):
|
|
168
|
+
filters.append(f'{prefix}{labels.node}="{params.get("node_name")}"')
|
|
169
|
+
|
|
170
|
+
filters.append(f'duration>{get_param_or_raise(params, "min_duration")}')
|
|
171
|
+
|
|
172
|
+
query = " && ".join(filters)
|
|
173
|
+
query = f"{{{query}}}"
|
|
174
|
+
|
|
175
|
+
base_url = get_base_url(self._toolset.grafana_config)
|
|
176
|
+
traces = query_tempo_traces(
|
|
177
|
+
base_url=base_url,
|
|
178
|
+
api_key=api_key,
|
|
179
|
+
headers=headers,
|
|
180
|
+
query=query,
|
|
181
|
+
start=start,
|
|
182
|
+
end=end,
|
|
183
|
+
limit=params.get("limit", 50),
|
|
184
|
+
)
|
|
185
|
+
return StructuredToolResult(
|
|
186
|
+
status=ToolResultStatus.SUCCESS,
|
|
187
|
+
data=format_traces_list(traces),
|
|
188
|
+
params=params,
|
|
189
|
+
invocation=query,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
193
|
+
return f"Fetched Tempo traces with min_duration={params.get('min_duration')} ({str(params)})"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class GetTempoTags(Tool):
|
|
197
|
+
def __init__(self, toolset: BaseGrafanaTempoToolset):
|
|
198
|
+
super().__init__(
|
|
199
|
+
name="fetch_tempo_tags",
|
|
200
|
+
description="List the tags available in Tempo",
|
|
201
|
+
parameters={
|
|
202
|
+
"start_datetime": ToolParameter(
|
|
203
|
+
description="The beginning time boundary for the search period. String in RFC3339 format. If a negative integer, the number of seconds relative to the end_timestamp. Defaults to negative 8 hours (-3600)",
|
|
204
|
+
type="string",
|
|
205
|
+
required=False,
|
|
206
|
+
),
|
|
207
|
+
"end_datetime": ToolParameter(
|
|
208
|
+
description="The ending time boundary for the search period. String in RFC3339 format. Defaults to NOW().",
|
|
209
|
+
type="string",
|
|
210
|
+
required=False,
|
|
211
|
+
),
|
|
212
|
+
},
|
|
213
|
+
)
|
|
214
|
+
self._toolset = toolset
|
|
215
|
+
|
|
216
|
+
def _invoke(self, params: Dict) -> StructuredToolResult:
|
|
217
|
+
api_key = self._toolset.grafana_config.api_key
|
|
218
|
+
headers = self._toolset.grafana_config.headers
|
|
219
|
+
start, end = process_timestamps_to_int(
|
|
220
|
+
start=params.get("start_datetime"),
|
|
221
|
+
end=params.get("end_datetime"),
|
|
222
|
+
default_time_span_seconds=8 * ONE_HOUR_IN_SECONDS,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
base_url = get_base_url(self._toolset.grafana_config)
|
|
226
|
+
url = f"{base_url}/api/v2/search/tags?start={start}&end={end}"
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
response = requests.get(
|
|
230
|
+
url,
|
|
231
|
+
headers=build_headers(api_key=api_key, additional_headers=headers),
|
|
232
|
+
timeout=60,
|
|
233
|
+
)
|
|
234
|
+
response.raise_for_status() # Raise an error for non-2xx responses
|
|
235
|
+
data = response.json()
|
|
236
|
+
return StructuredToolResult(
|
|
237
|
+
status=ToolResultStatus.SUCCESS,
|
|
238
|
+
data=yaml.dump(data.get("scopes")),
|
|
239
|
+
params=params,
|
|
240
|
+
)
|
|
241
|
+
except requests.exceptions.RequestException as e:
|
|
242
|
+
raise Exception(
|
|
243
|
+
f"Failed to retrieve trace by ID after retries: {e} \n for URL: {url}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
247
|
+
return f"Fetched Tempo tags ({str(params)})"
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class GetTempoTraceById(Tool):
|
|
251
|
+
def __init__(self, toolset: BaseGrafanaTempoToolset):
|
|
252
|
+
super().__init__(
|
|
253
|
+
name="fetch_tempo_trace_by_id",
|
|
254
|
+
description="""Retrieves detailed information about a Tempo trace using its trace ID. Use this to investigate a trace.""",
|
|
255
|
+
parameters={
|
|
256
|
+
"trace_id": ToolParameter(
|
|
257
|
+
description="The unique trace ID to fetch.",
|
|
258
|
+
type="string",
|
|
259
|
+
required=True,
|
|
260
|
+
),
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
self._toolset = toolset
|
|
264
|
+
|
|
265
|
+
def _invoke(self, params: Dict) -> StructuredToolResult:
|
|
266
|
+
labels_mapping = self._toolset.grafana_config.labels
|
|
267
|
+
labels = list(labels_mapping.model_dump().values())
|
|
268
|
+
|
|
269
|
+
base_url = get_base_url(self._toolset.grafana_config)
|
|
270
|
+
trace_data = query_tempo_trace_by_id(
|
|
271
|
+
base_url=base_url,
|
|
272
|
+
api_key=self._toolset.grafana_config.api_key,
|
|
273
|
+
headers=self._toolset.grafana_config.headers,
|
|
274
|
+
trace_id=get_param_or_raise(params, "trace_id"),
|
|
275
|
+
key_labels=labels,
|
|
276
|
+
)
|
|
277
|
+
return StructuredToolResult(
|
|
278
|
+
status=ToolResultStatus.SUCCESS,
|
|
279
|
+
data=trace_data,
|
|
280
|
+
params=params,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def get_parameterized_one_liner(self, params: Dict) -> str:
|
|
284
|
+
return f"Fetched Tempo trace with trace_id={params.get('trace_id')} ({str(params)})"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class GrafanaTempoToolset(BaseGrafanaTempoToolset):
|
|
288
|
+
def __init__(self):
|
|
289
|
+
super().__init__(
|
|
290
|
+
name="grafana/tempo",
|
|
291
|
+
description="Fetches kubernetes traces from Tempo",
|
|
292
|
+
icon_url="https://grafana.com/static/assets/img/blog/tempo.png",
|
|
293
|
+
docs_url="https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/grafanatempo.html",
|
|
294
|
+
tools=[GetTempoTraces(self), GetTempoTraceById(self), GetTempoTags(self)],
|
|
295
|
+
)
|
|
296
|
+
template_file_path = os.path.abspath(
|
|
297
|
+
os.path.join(os.path.dirname(__file__), "toolset_grafana_tempo.jinja2")
|
|
298
|
+
)
|
|
299
|
+
self._load_llm_instructions(jinja_template=f"file://{template_file_path}")
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Any
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
import base64
|
|
4
|
+
from holmes.plugins.toolsets.utils import unix_nano_to_rfc3339
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Span:
|
|
9
|
+
span_id: str
|
|
10
|
+
parent_span_id: Optional[str]
|
|
11
|
+
name: str
|
|
12
|
+
service_name: str
|
|
13
|
+
start_time: int
|
|
14
|
+
end_time: int
|
|
15
|
+
attributes: Dict[str, Any] = field(default_factory=dict)
|
|
16
|
+
events: List[Dict[str, Any]] = field(default_factory=list)
|
|
17
|
+
children: List["Span"] = field(default_factory=list)
|
|
18
|
+
resource_attributes: Dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def duration_ms(self) -> float:
|
|
22
|
+
"""Calculate duration in milliseconds"""
|
|
23
|
+
return (
|
|
24
|
+
self.end_time - self.start_time
|
|
25
|
+
) / 1_000_000 # Convert nanoseconds to milliseconds
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def decode_id(encoded_id: str) -> str:
|
|
29
|
+
"""Decode base64 IDs to a hex string for easier reading"""
|
|
30
|
+
return base64.b64decode(encoded_id).hex()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_span_hierarchy(trace_data: Dict) -> List[Span]:
|
|
34
|
+
# Step 1: Extract all spans and create span objects
|
|
35
|
+
all_spans = {}
|
|
36
|
+
|
|
37
|
+
for batch in trace_data["batches"]:
|
|
38
|
+
# Extract service name and other resource attributes
|
|
39
|
+
service_name = "unknown"
|
|
40
|
+
resource_attributes = {}
|
|
41
|
+
|
|
42
|
+
for attr in batch["resource"]["attributes"]:
|
|
43
|
+
key = attr["key"]
|
|
44
|
+
value = list(attr["value"].values())[0]
|
|
45
|
+
resource_attributes[key] = value
|
|
46
|
+
|
|
47
|
+
if key == "service.name":
|
|
48
|
+
service_name = value
|
|
49
|
+
|
|
50
|
+
for scope_spans in batch["scopeSpans"]:
|
|
51
|
+
for span_data in scope_spans["spans"]:
|
|
52
|
+
span_id = decode_id(span_data["spanId"])
|
|
53
|
+
|
|
54
|
+
# Get parent span ID if it exists
|
|
55
|
+
parent_span_id = None
|
|
56
|
+
if "parentSpanId" in span_data:
|
|
57
|
+
parent_span_id = decode_id(span_data["parentSpanId"])
|
|
58
|
+
|
|
59
|
+
# Create a Span object
|
|
60
|
+
span = Span(
|
|
61
|
+
span_id=span_id,
|
|
62
|
+
parent_span_id=parent_span_id,
|
|
63
|
+
name=span_data["name"],
|
|
64
|
+
service_name=service_name,
|
|
65
|
+
start_time=int(span_data["startTimeUnixNano"]),
|
|
66
|
+
end_time=int(span_data["endTimeUnixNano"]),
|
|
67
|
+
attributes={
|
|
68
|
+
attr["key"]: list(attr["value"].values())[0]
|
|
69
|
+
for attr in span_data.get("attributes", [])
|
|
70
|
+
},
|
|
71
|
+
events=span_data.get("events", []),
|
|
72
|
+
resource_attributes=resource_attributes,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
all_spans[span_id] = span
|
|
76
|
+
|
|
77
|
+
# Step 2: Build the hierarchy by connecting parents and children
|
|
78
|
+
root_spans = []
|
|
79
|
+
|
|
80
|
+
for span in all_spans.values():
|
|
81
|
+
if span.parent_span_id is None:
|
|
82
|
+
# This is a root span
|
|
83
|
+
root_spans.append(span)
|
|
84
|
+
else:
|
|
85
|
+
# Add this span as a child to its parent
|
|
86
|
+
if span.parent_span_id in all_spans:
|
|
87
|
+
all_spans[span.parent_span_id].children.append(span)
|
|
88
|
+
|
|
89
|
+
return root_spans
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def format_labels(attributes, key_labels):
|
|
93
|
+
"""Format the specified labels from attributes into a string"""
|
|
94
|
+
result = []
|
|
95
|
+
for key in key_labels:
|
|
96
|
+
if key in attributes:
|
|
97
|
+
result.append(f"{key}='{attributes[key]}'")
|
|
98
|
+
|
|
99
|
+
return " ".join(result) if result else None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def format_span_tree(span: Span, level: int = 0, key_labels: List[str] = []) -> str:
|
|
103
|
+
"""Format the span hierarchy in a tree format with timestamps and labels"""
|
|
104
|
+
span_tree_text = ""
|
|
105
|
+
indent = " " * level
|
|
106
|
+
duration = span.duration_ms
|
|
107
|
+
|
|
108
|
+
span_tree_text += f"{indent}├─ {span.name} ({span.service_name}) - {duration:.2f}ms (span_id={span.span_id})\n"
|
|
109
|
+
|
|
110
|
+
start_time_str = unix_nano_to_rfc3339(span.start_time)
|
|
111
|
+
end_time_str = unix_nano_to_rfc3339(span.end_time)
|
|
112
|
+
span_tree_text += (
|
|
113
|
+
f"{indent}│ Datetime: start={start_time_str} end={end_time_str}\n"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if key_labels and span.resource_attributes:
|
|
117
|
+
resource_labels = format_labels(span.resource_attributes, key_labels)
|
|
118
|
+
if resource_labels:
|
|
119
|
+
span_tree_text += f"{indent}│ Resource labels: {resource_labels}\n"
|
|
120
|
+
|
|
121
|
+
if span.attributes:
|
|
122
|
+
span_tree_text += f"{indent}│ Attributes:\n"
|
|
123
|
+
for key, value in span.attributes.items():
|
|
124
|
+
span_tree_text += f"{indent}│ {key}: {value}\n"
|
|
125
|
+
|
|
126
|
+
if key_labels:
|
|
127
|
+
span_labels = format_labels(span.attributes, key_labels)
|
|
128
|
+
if span_labels:
|
|
129
|
+
span_tree_text += f"{indent}│ Span labels: {span_labels}\n"
|
|
130
|
+
|
|
131
|
+
if span.events:
|
|
132
|
+
span_tree_text += f"{indent}│ Events:\n"
|
|
133
|
+
for event in span.events:
|
|
134
|
+
event_name = event["name"]
|
|
135
|
+
event_time = int(event["timeUnixNano"])
|
|
136
|
+
event_time_str = unix_nano_to_rfc3339(event_time)
|
|
137
|
+
relative_time = (event_time - span.start_time) / 1_000_000
|
|
138
|
+
span_tree_text += f"{indent}│ {event_name} (+{relative_time:.2f}ms) at {event_time_str}\n"
|
|
139
|
+
|
|
140
|
+
if "attributes" in event and event["attributes"]:
|
|
141
|
+
for attr in event["attributes"]:
|
|
142
|
+
attr_key = attr["key"]
|
|
143
|
+
values = list(attr["value"].values())
|
|
144
|
+
if len(values) == 1:
|
|
145
|
+
span_tree_text += (
|
|
146
|
+
f"{indent}│ {attr_key}: {str(values[0])}\n"
|
|
147
|
+
)
|
|
148
|
+
elif values:
|
|
149
|
+
span_tree_text += f"{indent}│ {attr_key}: {str(values)}\n"
|
|
150
|
+
|
|
151
|
+
for child in sorted(span.children, key=lambda s: s.start_time):
|
|
152
|
+
span_tree_text += format_span_tree(child, level + 1, key_labels)
|
|
153
|
+
|
|
154
|
+
return span_tree_text
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def process_trace(
|
|
158
|
+
trace_data: Dict,
|
|
159
|
+
key_labels: List[str] = [
|
|
160
|
+
"service.name",
|
|
161
|
+
"service.version",
|
|
162
|
+
"k8s.deployment.name",
|
|
163
|
+
"k8s.node.name",
|
|
164
|
+
"k8s.pod.name",
|
|
165
|
+
"k8s.namespace.name",
|
|
166
|
+
],
|
|
167
|
+
) -> str:
|
|
168
|
+
root_spans = build_span_hierarchy(trace_data)
|
|
169
|
+
|
|
170
|
+
span_trees = []
|
|
171
|
+
for root_span in sorted(root_spans, key=lambda s: s.start_time):
|
|
172
|
+
span_trees.append(format_span_tree(root_span, key_labels=key_labels))
|
|
173
|
+
|
|
174
|
+
return "\n\n".join(span_trees)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def format_traces_list(trace_data: Dict) -> str:
|
|
178
|
+
traces = trace_data.get("traces", [])
|
|
179
|
+
|
|
180
|
+
if len(traces) > 0:
|
|
181
|
+
traces_str = []
|
|
182
|
+
for trace in traces:
|
|
183
|
+
trace_str = f"Trace (traceID={trace.get('traceID')})"
|
|
184
|
+
trace_str += (
|
|
185
|
+
f" (durationMs={trace.get('durationMs')})\n"
|
|
186
|
+
if trace.get("durationMs") is not None
|
|
187
|
+
else "\n"
|
|
188
|
+
)
|
|
189
|
+
trace_str += f"\tstartTime={unix_nano_to_rfc3339(int(trace.get('startTimeUnixNano')))}"
|
|
190
|
+
trace_str += f" rootServiceName={trace.get('trootServiceName')}"
|
|
191
|
+
trace_str += f" rootTraceName={trace.get('rootTraceName')}"
|
|
192
|
+
traces_str.append(trace_str)
|
|
193
|
+
return "\n".join(traces_str)
|
|
194
|
+
else:
|
|
195
|
+
return "No matching trace could be found"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
toolsets:
|
|
2
|
+
helm/core:
|
|
3
|
+
description: "Read access to cluster's Helm charts and releases"
|
|
4
|
+
docs_url: "https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/helm.html"
|
|
5
|
+
icon_url: "https://helm.sh/img/helm.svg"
|
|
6
|
+
tags:
|
|
7
|
+
- core
|
|
8
|
+
prerequisites:
|
|
9
|
+
- command: "helm version"
|
|
10
|
+
|
|
11
|
+
tools:
|
|
12
|
+
- name: "helm_list"
|
|
13
|
+
description: "Use to get all the current helm releases"
|
|
14
|
+
command: "helm list"
|
|
15
|
+
|
|
16
|
+
- name: "helm_values"
|
|
17
|
+
description: "Use to gather Helm values or any released helm chart"
|
|
18
|
+
command: "helm get values -a {{ release_name }} -n {{ namespace }} -o json"
|
|
19
|
+
|
|
20
|
+
- name: "helm_status"
|
|
21
|
+
description: "Check the status of a Helm release"
|
|
22
|
+
command: "helm status {{ release_name }} -n {{ namespace }}"
|
|
23
|
+
|
|
24
|
+
- name: "helm_history"
|
|
25
|
+
description: "Get the revision history of a Helm release"
|
|
26
|
+
command: "helm history {{ release_name }} -n {{ namespace }}"
|
|
27
|
+
|
|
28
|
+
- name: "helm_manifest"
|
|
29
|
+
description: "Fetch the generated Kubernetes manifest for a Helm release"
|
|
30
|
+
command: "helm get manifest {{ release_name }} -n {{ namespace }}"
|
|
31
|
+
|
|
32
|
+
- name: "helm_hooks"
|
|
33
|
+
description: "Get the hooks for a Helm release"
|
|
34
|
+
command: "helm get hooks {{ release_name }} -n {{ namespace }}"
|
|
35
|
+
|
|
36
|
+
- name: "helm_chart"
|
|
37
|
+
description: "Show the chart used to create a Helm release"
|
|
38
|
+
command: "helm get chart {{ release_name }} -n {{ namespace }}"
|
|
39
|
+
|
|
40
|
+
- name: "helm_notes"
|
|
41
|
+
description: "Show the notes provided by the Helm chart"
|
|
42
|
+
command: "helm get notes {{ release_name }} -n {{ namespace }}"
|