holmesgpt 0.11.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (183) hide show
  1. holmes/.git_archival.json +7 -0
  2. holmes/__init__.py +76 -0
  3. holmes/__init__.py.bak +76 -0
  4. holmes/clients/robusta_client.py +24 -0
  5. holmes/common/env_vars.py +47 -0
  6. holmes/config.py +526 -0
  7. holmes/core/__init__.py +0 -0
  8. holmes/core/conversations.py +578 -0
  9. holmes/core/investigation.py +152 -0
  10. holmes/core/investigation_structured_output.py +264 -0
  11. holmes/core/issue.py +54 -0
  12. holmes/core/llm.py +250 -0
  13. holmes/core/models.py +157 -0
  14. holmes/core/openai_formatting.py +51 -0
  15. holmes/core/performance_timing.py +72 -0
  16. holmes/core/prompt.py +42 -0
  17. holmes/core/resource_instruction.py +17 -0
  18. holmes/core/runbooks.py +26 -0
  19. holmes/core/safeguards.py +120 -0
  20. holmes/core/supabase_dal.py +540 -0
  21. holmes/core/tool_calling_llm.py +798 -0
  22. holmes/core/tools.py +566 -0
  23. holmes/core/tools_utils/__init__.py +0 -0
  24. holmes/core/tools_utils/tool_executor.py +65 -0
  25. holmes/core/tools_utils/toolset_utils.py +52 -0
  26. holmes/core/toolset_manager.py +418 -0
  27. holmes/interactive.py +229 -0
  28. holmes/main.py +1041 -0
  29. holmes/plugins/__init__.py +0 -0
  30. holmes/plugins/destinations/__init__.py +6 -0
  31. holmes/plugins/destinations/slack/__init__.py +2 -0
  32. holmes/plugins/destinations/slack/plugin.py +163 -0
  33. holmes/plugins/interfaces.py +32 -0
  34. holmes/plugins/prompts/__init__.py +48 -0
  35. holmes/plugins/prompts/_current_date_time.jinja2 +1 -0
  36. holmes/plugins/prompts/_default_log_prompt.jinja2 +11 -0
  37. holmes/plugins/prompts/_fetch_logs.jinja2 +36 -0
  38. holmes/plugins/prompts/_general_instructions.jinja2 +86 -0
  39. holmes/plugins/prompts/_global_instructions.jinja2 +12 -0
  40. holmes/plugins/prompts/_runbook_instructions.jinja2 +13 -0
  41. holmes/plugins/prompts/_toolsets_instructions.jinja2 +56 -0
  42. holmes/plugins/prompts/generic_ask.jinja2 +36 -0
  43. holmes/plugins/prompts/generic_ask_conversation.jinja2 +32 -0
  44. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +50 -0
  45. holmes/plugins/prompts/generic_investigation.jinja2 +42 -0
  46. holmes/plugins/prompts/generic_post_processing.jinja2 +13 -0
  47. holmes/plugins/prompts/generic_ticket.jinja2 +12 -0
  48. holmes/plugins/prompts/investigation_output_format.jinja2 +32 -0
  49. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +84 -0
  50. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +39 -0
  51. holmes/plugins/runbooks/README.md +22 -0
  52. holmes/plugins/runbooks/__init__.py +100 -0
  53. holmes/plugins/runbooks/catalog.json +14 -0
  54. holmes/plugins/runbooks/jira.yaml +12 -0
  55. holmes/plugins/runbooks/kube-prometheus-stack.yaml +10 -0
  56. holmes/plugins/runbooks/networking/dns_troubleshooting_instructions.md +66 -0
  57. holmes/plugins/runbooks/upgrade/upgrade_troubleshooting_instructions.md +44 -0
  58. holmes/plugins/sources/github/__init__.py +77 -0
  59. holmes/plugins/sources/jira/__init__.py +123 -0
  60. holmes/plugins/sources/opsgenie/__init__.py +93 -0
  61. holmes/plugins/sources/pagerduty/__init__.py +147 -0
  62. holmes/plugins/sources/prometheus/__init__.py +0 -0
  63. holmes/plugins/sources/prometheus/models.py +104 -0
  64. holmes/plugins/sources/prometheus/plugin.py +154 -0
  65. holmes/plugins/toolsets/__init__.py +171 -0
  66. holmes/plugins/toolsets/aks-node-health.yaml +65 -0
  67. holmes/plugins/toolsets/aks.yaml +86 -0
  68. holmes/plugins/toolsets/argocd.yaml +70 -0
  69. holmes/plugins/toolsets/atlas_mongodb/instructions.jinja2 +8 -0
  70. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +307 -0
  71. holmes/plugins/toolsets/aws.yaml +76 -0
  72. holmes/plugins/toolsets/azure_sql/__init__.py +0 -0
  73. holmes/plugins/toolsets/azure_sql/apis/alert_monitoring_api.py +600 -0
  74. holmes/plugins/toolsets/azure_sql/apis/azure_sql_api.py +309 -0
  75. holmes/plugins/toolsets/azure_sql/apis/connection_failure_api.py +445 -0
  76. holmes/plugins/toolsets/azure_sql/apis/connection_monitoring_api.py +251 -0
  77. holmes/plugins/toolsets/azure_sql/apis/storage_analysis_api.py +317 -0
  78. holmes/plugins/toolsets/azure_sql/azure_base_toolset.py +55 -0
  79. holmes/plugins/toolsets/azure_sql/azure_sql_instructions.jinja2 +137 -0
  80. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +183 -0
  81. holmes/plugins/toolsets/azure_sql/install.md +66 -0
  82. holmes/plugins/toolsets/azure_sql/tools/__init__.py +1 -0
  83. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +324 -0
  84. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +243 -0
  85. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +205 -0
  86. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +249 -0
  87. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +373 -0
  88. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +237 -0
  89. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +172 -0
  90. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +170 -0
  91. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +188 -0
  92. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +180 -0
  93. holmes/plugins/toolsets/azure_sql/utils.py +83 -0
  94. holmes/plugins/toolsets/bash/__init__.py +0 -0
  95. holmes/plugins/toolsets/bash/bash_instructions.jinja2 +14 -0
  96. holmes/plugins/toolsets/bash/bash_toolset.py +208 -0
  97. holmes/plugins/toolsets/bash/common/bash.py +52 -0
  98. holmes/plugins/toolsets/bash/common/config.py +14 -0
  99. holmes/plugins/toolsets/bash/common/stringify.py +25 -0
  100. holmes/plugins/toolsets/bash/common/validators.py +24 -0
  101. holmes/plugins/toolsets/bash/grep/__init__.py +52 -0
  102. holmes/plugins/toolsets/bash/kubectl/__init__.py +100 -0
  103. holmes/plugins/toolsets/bash/kubectl/constants.py +96 -0
  104. holmes/plugins/toolsets/bash/kubectl/kubectl_describe.py +66 -0
  105. holmes/plugins/toolsets/bash/kubectl/kubectl_events.py +88 -0
  106. holmes/plugins/toolsets/bash/kubectl/kubectl_get.py +108 -0
  107. holmes/plugins/toolsets/bash/kubectl/kubectl_logs.py +20 -0
  108. holmes/plugins/toolsets/bash/kubectl/kubectl_run.py +46 -0
  109. holmes/plugins/toolsets/bash/kubectl/kubectl_top.py +81 -0
  110. holmes/plugins/toolsets/bash/parse_command.py +103 -0
  111. holmes/plugins/toolsets/confluence.yaml +19 -0
  112. holmes/plugins/toolsets/consts.py +5 -0
  113. holmes/plugins/toolsets/coralogix/api.py +158 -0
  114. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +103 -0
  115. holmes/plugins/toolsets/coralogix/utils.py +181 -0
  116. holmes/plugins/toolsets/datadog.py +153 -0
  117. holmes/plugins/toolsets/docker.yaml +46 -0
  118. holmes/plugins/toolsets/git.py +756 -0
  119. holmes/plugins/toolsets/grafana/__init__.py +0 -0
  120. holmes/plugins/toolsets/grafana/base_grafana_toolset.py +54 -0
  121. holmes/plugins/toolsets/grafana/common.py +68 -0
  122. holmes/plugins/toolsets/grafana/grafana_api.py +31 -0
  123. holmes/plugins/toolsets/grafana/loki_api.py +89 -0
  124. holmes/plugins/toolsets/grafana/tempo_api.py +124 -0
  125. holmes/plugins/toolsets/grafana/toolset_grafana.py +102 -0
  126. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +102 -0
  127. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +10 -0
  128. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +299 -0
  129. holmes/plugins/toolsets/grafana/trace_parser.py +195 -0
  130. holmes/plugins/toolsets/helm.yaml +42 -0
  131. holmes/plugins/toolsets/internet/internet.py +275 -0
  132. holmes/plugins/toolsets/internet/notion.py +137 -0
  133. holmes/plugins/toolsets/kafka.py +638 -0
  134. holmes/plugins/toolsets/kubernetes.yaml +255 -0
  135. holmes/plugins/toolsets/kubernetes_logs.py +426 -0
  136. holmes/plugins/toolsets/kubernetes_logs.yaml +42 -0
  137. holmes/plugins/toolsets/logging_utils/__init__.py +0 -0
  138. holmes/plugins/toolsets/logging_utils/logging_api.py +217 -0
  139. holmes/plugins/toolsets/logging_utils/types.py +0 -0
  140. holmes/plugins/toolsets/mcp/toolset_mcp.py +135 -0
  141. holmes/plugins/toolsets/newrelic.py +222 -0
  142. holmes/plugins/toolsets/opensearch/__init__.py +0 -0
  143. holmes/plugins/toolsets/opensearch/opensearch.py +245 -0
  144. holmes/plugins/toolsets/opensearch/opensearch_logs.py +151 -0
  145. holmes/plugins/toolsets/opensearch/opensearch_traces.py +211 -0
  146. holmes/plugins/toolsets/opensearch/opensearch_traces_instructions.jinja2 +12 -0
  147. holmes/plugins/toolsets/opensearch/opensearch_utils.py +166 -0
  148. holmes/plugins/toolsets/prometheus/prometheus.py +818 -0
  149. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +38 -0
  150. holmes/plugins/toolsets/rabbitmq/api.py +398 -0
  151. holmes/plugins/toolsets/rabbitmq/rabbitmq_instructions.jinja2 +37 -0
  152. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +222 -0
  153. holmes/plugins/toolsets/robusta/__init__.py +0 -0
  154. holmes/plugins/toolsets/robusta/robusta.py +235 -0
  155. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +24 -0
  156. holmes/plugins/toolsets/runbook/__init__.py +0 -0
  157. holmes/plugins/toolsets/runbook/runbook_fetcher.py +78 -0
  158. holmes/plugins/toolsets/service_discovery.py +92 -0
  159. holmes/plugins/toolsets/servicenow/install.md +37 -0
  160. holmes/plugins/toolsets/servicenow/instructions.jinja2 +3 -0
  161. holmes/plugins/toolsets/servicenow/servicenow.py +198 -0
  162. holmes/plugins/toolsets/slab.yaml +20 -0
  163. holmes/plugins/toolsets/utils.py +137 -0
  164. holmes/plugins/utils.py +14 -0
  165. holmes/utils/__init__.py +0 -0
  166. holmes/utils/cache.py +84 -0
  167. holmes/utils/cert_utils.py +40 -0
  168. holmes/utils/default_toolset_installation_guide.jinja2 +44 -0
  169. holmes/utils/definitions.py +13 -0
  170. holmes/utils/env.py +53 -0
  171. holmes/utils/file_utils.py +56 -0
  172. holmes/utils/global_instructions.py +20 -0
  173. holmes/utils/holmes_status.py +22 -0
  174. holmes/utils/holmes_sync_toolsets.py +80 -0
  175. holmes/utils/markdown_utils.py +55 -0
  176. holmes/utils/pydantic_utils.py +54 -0
  177. holmes/utils/robusta.py +10 -0
  178. holmes/utils/tags.py +97 -0
  179. holmesgpt-0.11.5.dist-info/LICENSE.txt +21 -0
  180. holmesgpt-0.11.5.dist-info/METADATA +400 -0
  181. holmesgpt-0.11.5.dist-info/RECORD +183 -0
  182. holmesgpt-0.11.5.dist-info/WHEEL +4 -0
  183. holmesgpt-0.11.5.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,245 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Optional, Tuple
3
+
4
+ from opensearchpy import OpenSearch
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from holmes.core.tools import (
8
+ CallablePrerequisite,
9
+ StructuredToolResult,
10
+ Tool,
11
+ ToolParameter,
12
+ ToolResultStatus,
13
+ Toolset,
14
+ ToolsetTag,
15
+ )
16
+ from holmes.plugins.toolsets.consts import TOOLSET_CONFIG_MISSING_ERROR
17
+
18
+
19
+ class OpenSearchHttpAuth(BaseModel):
20
+ username: str
21
+ password: str
22
+
23
+
24
+ class OpenSearchHost(BaseModel):
25
+ host: str
26
+ port: int = 9200
27
+
28
+
29
+ class OpenSearchCluster(BaseModel):
30
+ hosts: list[OpenSearchHost]
31
+ headers: Optional[dict[str, Any]] = None
32
+ use_ssl: bool = True
33
+ ssl_assert_hostname: bool = False
34
+ verify_certs: bool = False
35
+ ssl_show_warn: bool = False
36
+ http_auth: Optional[OpenSearchHttpAuth] = None
37
+
38
+
39
+ class OpenSearchConfig(BaseModel):
40
+ opensearch_clusters: list[OpenSearchCluster]
41
+
42
+
43
+ class OpenSearchClient:
44
+ def __init__(self, **kwargs):
45
+ # Handle http_auth explicitly
46
+ if "http_auth" in kwargs:
47
+ http_auth = kwargs.pop("http_auth")
48
+ if isinstance(http_auth, dict):
49
+ kwargs["http_auth"] = (
50
+ http_auth.get("username"),
51
+ http_auth.get("password"),
52
+ )
53
+ # Initialize OpenSearch client
54
+ self.hosts = [host.get("host") for host in kwargs.get("hosts", [])]
55
+ self.client = OpenSearch(**kwargs)
56
+
57
+
58
+ def get_client(clients: List[OpenSearchClient], host: Optional[str]):
59
+ if len(clients) == 1:
60
+ return clients[0]
61
+
62
+ if not host:
63
+ raise Exception("Missing host to resolve opensearch client")
64
+
65
+ for client in clients:
66
+ found = any(host in client.hosts for client in clients)
67
+ if found:
68
+ return client
69
+
70
+ raise Exception(
71
+ f"Failed to resolve opensearch client. Could not find a matching host: {host}"
72
+ )
73
+
74
+
75
+ class BaseOpenSearchTool(Tool):
76
+ model_config = ConfigDict(arbitrary_types_allowed=True)
77
+ toolset: "OpenSearchToolset"
78
+
79
+
80
+ class ListShards(BaseOpenSearchTool):
81
+ def __init__(self, toolset: "OpenSearchToolset"):
82
+ super().__init__(
83
+ name="opensearch_list_shards",
84
+ description="List the shards within an opensearch cluster",
85
+ parameters={
86
+ "host": ToolParameter(
87
+ description="The cluster host",
88
+ type="string",
89
+ required=True,
90
+ )
91
+ },
92
+ toolset=toolset,
93
+ )
94
+
95
+ def _invoke(self, params: Any) -> StructuredToolResult:
96
+ client = get_client(self.toolset.clients, host=params.get("host", ""))
97
+ shards = client.client.cat.shards()
98
+ return StructuredToolResult(
99
+ status=ToolResultStatus.SUCCESS,
100
+ data=str(shards),
101
+ params=params,
102
+ )
103
+
104
+ def get_parameterized_one_liner(self, params: Dict) -> str:
105
+ return f"opensearch ListShards({params.get('host')})"
106
+
107
+
108
+ class GetClusterSettings(BaseOpenSearchTool):
109
+ def __init__(self, toolset: "OpenSearchToolset"):
110
+ super().__init__(
111
+ name="opensearch_get_cluster_settings",
112
+ description="Retrieve the cluster's settings",
113
+ parameters={
114
+ "host": ToolParameter(
115
+ description="The cluster host",
116
+ type="string",
117
+ required=True,
118
+ )
119
+ },
120
+ toolset=toolset,
121
+ )
122
+
123
+ def _invoke(self, params: Any) -> StructuredToolResult:
124
+ client = get_client(self.toolset.clients, host=params.get("host"))
125
+ response = client.client.cluster.get_settings(
126
+ include_defaults=True, flat_settings=True
127
+ )
128
+ return StructuredToolResult(
129
+ status=ToolResultStatus.SUCCESS,
130
+ data=str(response),
131
+ params=params,
132
+ )
133
+
134
+ def get_parameterized_one_liner(self, params) -> str:
135
+ return f"opensearch GetClusterSettings({params.get('host')})"
136
+
137
+
138
+ class GetClusterHealth(BaseOpenSearchTool):
139
+ def __init__(self, toolset: "OpenSearchToolset"):
140
+ super().__init__(
141
+ name="opensearch_get_cluster_health",
142
+ description="Retrieve the cluster's health",
143
+ parameters={
144
+ "host": ToolParameter(
145
+ description="The cluster host",
146
+ type="string",
147
+ required=True,
148
+ )
149
+ },
150
+ toolset=toolset,
151
+ )
152
+
153
+ def _invoke(self, params: Any) -> StructuredToolResult:
154
+ client = get_client(self.toolset.clients, host=params.get("host", ""))
155
+ health = client.client.cluster.health()
156
+ return StructuredToolResult(
157
+ status=ToolResultStatus.SUCCESS,
158
+ data=str(health),
159
+ params=params,
160
+ )
161
+
162
+ def get_parameterized_one_liner(self, params) -> str:
163
+ return f"opensearch GetClusterHealth({params.get('host')})"
164
+
165
+
166
+ class ListOpenSearchHosts(BaseOpenSearchTool):
167
+ def __init__(self, toolset: "OpenSearchToolset"):
168
+ super().__init__(
169
+ name="opensearch_list_hosts",
170
+ description="List all OpenSearch hosts in the cluster",
171
+ parameters={},
172
+ toolset=toolset,
173
+ )
174
+
175
+ def _invoke(self, params: Any) -> StructuredToolResult:
176
+ hosts = [host for client in self.toolset.clients for host in client.hosts]
177
+ return StructuredToolResult(
178
+ status=ToolResultStatus.SUCCESS,
179
+ data=str(hosts),
180
+ params=params,
181
+ )
182
+
183
+ def get_parameterized_one_liner(self, params: Dict) -> str:
184
+ return "opensearch ListOpenSearchHosts()"
185
+
186
+
187
+ class OpenSearchToolset(Toolset):
188
+ model_config = ConfigDict(arbitrary_types_allowed=True)
189
+ clients: List[OpenSearchClient] = []
190
+
191
+ def __init__(self):
192
+ super().__init__(
193
+ name="opensearch/status",
194
+ enabled=False,
195
+ description="Provide cluster metadata information like health, shards, settings.",
196
+ docs_url="https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/opensearch.html",
197
+ icon_url="https://opensearch.org/assets/brand/PNG/Mark/opensearch_mark_default.png",
198
+ prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
199
+ tools=[
200
+ ListShards(self),
201
+ GetClusterSettings(self),
202
+ GetClusterHealth(self),
203
+ ListOpenSearchHosts(self),
204
+ ],
205
+ tags=[
206
+ ToolsetTag.CORE,
207
+ ],
208
+ )
209
+
210
+ def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
211
+ if not config:
212
+ return False, TOOLSET_CONFIG_MISSING_ERROR
213
+
214
+ try:
215
+ os_config = OpenSearchConfig(**config)
216
+ errors = []
217
+ for cluster in os_config.opensearch_clusters:
218
+ try:
219
+ logging.info("Setting up OpenSearch client")
220
+ cluster_kwargs = cluster.model_dump()
221
+ client = OpenSearchClient(**cluster_kwargs)
222
+ if client.client.cluster.health(params={"timeout": 5}):
223
+ self.clients.append(client)
224
+ except Exception as e:
225
+ message = f"Failed to set up opensearch client {str(cluster.hosts)}. {str(e)}"
226
+ logging.exception(message)
227
+ errors.append(message)
228
+
229
+ return len(self.clients) > 0, "\n".join(errors)
230
+ except Exception as e:
231
+ logging.exception("Failed to set up OpenSearch toolset")
232
+ return False, str(e)
233
+
234
+ def get_example_config(self) -> Dict[str, Any]:
235
+ example_config = OpenSearchConfig(
236
+ opensearch_clusters=[
237
+ OpenSearchCluster(
238
+ hosts=[OpenSearchHost(host="YOUR OPENSEACH HOST")],
239
+ headers={"Authorization": "{{ env.OPENSEARCH_BEARER_TOKEN }}"},
240
+ use_ssl=True,
241
+ ssl_assert_hostname=False,
242
+ )
243
+ ]
244
+ )
245
+ return example_config.model_dump()
@@ -0,0 +1,151 @@
1
+ import logging
2
+ from typing import Any, Dict, Optional, Tuple
3
+
4
+ import requests # type: ignore
5
+ from requests import RequestException # type: ignore
6
+ from urllib.parse import urljoin
7
+
8
+ from holmes.core.tools import (
9
+ CallablePrerequisite,
10
+ StructuredToolResult,
11
+ ToolResultStatus,
12
+ ToolsetTag,
13
+ )
14
+ from holmes.plugins.toolsets.logging_utils.logging_api import (
15
+ BasePodLoggingToolset,
16
+ FetchPodLogsParams,
17
+ PodLoggingTool,
18
+ process_time_parameters,
19
+ )
20
+ from holmes.plugins.toolsets.opensearch.opensearch_utils import (
21
+ OpenSearchLoggingConfig,
22
+ add_auth_header,
23
+ build_query,
24
+ format_logs,
25
+ opensearch_health_check,
26
+ )
27
+
28
+ LOGS_FIELDS_CACHE_KEY = "cached_logs_fields"
29
+
30
+
31
+ class OpenSearchLogsToolset(BasePodLoggingToolset):
32
+ """Implementation of the unified logging API for OpenSearch logs"""
33
+
34
+ def __init__(self):
35
+ super().__init__(
36
+ name="opensearch/logs",
37
+ description="OpenSearch integration to fetch logs",
38
+ docs_url="https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/opensearch_logs.html",
39
+ icon_url="https://opensearch.org/wp-content/uploads/2025/01/opensearch_mark_default.png",
40
+ prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
41
+ tools=[
42
+ PodLoggingTool(self),
43
+ ],
44
+ tags=[
45
+ ToolsetTag.CORE,
46
+ ],
47
+ )
48
+
49
+ def get_example_config(self) -> Dict[str, Any]:
50
+ example_config = OpenSearchLoggingConfig(
51
+ opensearch_url="YOUR OPENSEARCH LOGS URL",
52
+ index_pattern="YOUR OPENSEARCH LOGS INDEX NAME",
53
+ opensearch_auth_header="YOUR OPENSEARCH LOGS AUTH HEADER (Optional)",
54
+ )
55
+ return example_config.model_dump()
56
+
57
+ def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
58
+ if not config:
59
+ return False, "Missing OpenSearch configuration. Check your config."
60
+
61
+ self.config = OpenSearchLoggingConfig(**config)
62
+
63
+ return opensearch_health_check(self.config)
64
+
65
+ @property
66
+ def opensearch_config(self) -> Optional[OpenSearchLoggingConfig]:
67
+ return self.config
68
+
69
+ def fetch_pod_logs(self, params: FetchPodLogsParams) -> StructuredToolResult:
70
+ if not self.opensearch_config:
71
+ return StructuredToolResult(
72
+ status=ToolResultStatus.ERROR,
73
+ error="Missing OpenSearch configuration",
74
+ params=params.model_dump(),
75
+ )
76
+
77
+ try:
78
+ start_time = None
79
+ end_time = None
80
+ if params.start_time or params.end_time:
81
+ start_time, end_time = process_time_parameters(
82
+ params.start_time, params.end_time
83
+ )
84
+
85
+ query = build_query(
86
+ config=self.opensearch_config,
87
+ namespace=params.namespace,
88
+ pod_name=params.pod_name,
89
+ start_time=start_time,
90
+ end_time=end_time,
91
+ match=params.filter,
92
+ limit=params.limit,
93
+ )
94
+
95
+ headers = {"Content-Type": "application/json"}
96
+ headers.update(
97
+ add_auth_header(self.opensearch_config.opensearch_auth_header)
98
+ )
99
+
100
+ url = urljoin(
101
+ self.opensearch_config.opensearch_url,
102
+ f"/{self.opensearch_config.index_pattern}/_search",
103
+ )
104
+ logs_response = requests.post(
105
+ url=url,
106
+ timeout=180,
107
+ verify=True,
108
+ json=query,
109
+ headers=headers,
110
+ )
111
+
112
+ if logs_response.status_code == 200:
113
+ response = logs_response.json()
114
+ logs = format_logs(
115
+ logs=response.get("hits", {}).get("hits", []),
116
+ config=self.opensearch_config,
117
+ )
118
+ return StructuredToolResult(
119
+ status=ToolResultStatus.SUCCESS,
120
+ data=logs,
121
+ params=params.model_dump(),
122
+ )
123
+ else:
124
+ return StructuredToolResult(
125
+ status=ToolResultStatus.ERROR,
126
+ return_code=logs_response.status_code,
127
+ error=logs_response.text,
128
+ params=params.model_dump(),
129
+ )
130
+
131
+ except requests.Timeout:
132
+ logging.warning("Timeout while fetching OpenSearch logs", exc_info=True)
133
+ return StructuredToolResult(
134
+ status=ToolResultStatus.ERROR,
135
+ error="Request timed out while fetching OpenSearch logs",
136
+ params=params.model_dump(),
137
+ )
138
+ except RequestException as e:
139
+ logging.warning("Failed to fetch OpenSearch logs", exc_info=True)
140
+ return StructuredToolResult(
141
+ status=ToolResultStatus.ERROR,
142
+ error=f"Network error while fetching OpenSearch logs: {str(e)}",
143
+ params=params.model_dump(),
144
+ )
145
+ except Exception as e:
146
+ logging.warning("Failed to process OpenSearch logs", exc_info=True)
147
+ return StructuredToolResult(
148
+ status=ToolResultStatus.ERROR,
149
+ error=f"Unexpected error: {str(e)}",
150
+ params=params.model_dump(),
151
+ )
@@ -0,0 +1,211 @@
1
+ import os
2
+ import logging
3
+
4
+ from typing import Any, Dict
5
+
6
+ import requests # type: ignore
7
+ from cachetools import TTLCache # type: ignore
8
+ from holmes.core.tools import (
9
+ CallablePrerequisite,
10
+ Tool,
11
+ ToolParameter,
12
+ ToolsetTag,
13
+ )
14
+ import json
15
+ from requests import RequestException
16
+
17
+ from holmes.plugins.toolsets.opensearch.opensearch_utils import (
18
+ BaseOpenSearchToolset,
19
+ add_auth_header,
20
+ get_search_url,
21
+ )
22
+ from holmes.core.tools import StructuredToolResult, ToolResultStatus
23
+
24
+ TRACES_FIELDS_CACHE_KEY = "cached_traces_fields"
25
+
26
+
27
+ class GetTracesFields(Tool):
28
+ def __init__(self, toolset: BaseOpenSearchToolset):
29
+ super().__init__(
30
+ name="get_traces_fields",
31
+ description="Get all the fields in the traces documents",
32
+ parameters={},
33
+ )
34
+ self._toolset = toolset
35
+ self._cache = None
36
+
37
+ def _invoke(self, params: Dict) -> StructuredToolResult:
38
+ try:
39
+ if not self._cache and self._toolset.opensearch_config.fields_ttl_seconds:
40
+ self._cache = TTLCache(
41
+ maxsize=5, ttl=self._toolset.opensearch_config.fields_ttl_seconds
42
+ )
43
+
44
+ if self._cache:
45
+ cached_response = self._cache.get(TRACES_FIELDS_CACHE_KEY, None)
46
+ if cached_response:
47
+ logging.debug("traces fields returned from cache")
48
+ return StructuredToolResult(
49
+ status=ToolResultStatus.SUCCESS,
50
+ data=cached_response,
51
+ params=params,
52
+ )
53
+
54
+ body = {
55
+ "size": 1,
56
+ "_source": False,
57
+ "script_fields": {
58
+ "all_fields": {
59
+ "script": {
60
+ "lang": "painless",
61
+ "source": "Map fields = new HashMap(); List stack = new ArrayList(); stack.add(['prefix': '', 'obj': params['_source']]); while (!stack.isEmpty()) { Map item = stack.remove(stack.size() - 1); String prefix = item.get('prefix'); Map obj = item.get('obj'); for (entry in obj.entrySet()) { String fullPath = prefix.isEmpty() ? entry.getKey() : prefix + '.' + entry.getKey(); fields.put(fullPath, true); if (entry.getValue() instanceof Map) { stack.add(['prefix': fullPath, 'obj': entry.getValue()]); } } } return fields.keySet();",
62
+ }
63
+ }
64
+ },
65
+ }
66
+ headers = {"Content-Type": "application/json"}
67
+ headers.update(
68
+ add_auth_header(self._toolset.opensearch_config.opensearch_auth_header)
69
+ )
70
+ logs_response = requests.get(
71
+ url=get_search_url(self._toolset.opensearch_config),
72
+ timeout=180,
73
+ verify=True,
74
+ data=json.dumps(body),
75
+ headers=headers,
76
+ )
77
+ logs_response.raise_for_status()
78
+ response = json.dumps(logs_response.json())
79
+ if self._cache:
80
+ self._cache[TRACES_FIELDS_CACHE_KEY] = response
81
+ return StructuredToolResult(
82
+ status=ToolResultStatus.SUCCESS,
83
+ data=response,
84
+ params=params,
85
+ )
86
+ except requests.Timeout:
87
+ logging.warning(
88
+ "Timeout while fetching opensearch traces fields", exc_info=True
89
+ )
90
+ return StructuredToolResult(
91
+ status=ToolResultStatus.ERROR,
92
+ error="Request timed out while fetching opensearch traces fields",
93
+ params=params,
94
+ )
95
+ except RequestException as e:
96
+ logging.warning("Failed to fetch opensearch traces fields", exc_info=True)
97
+ return StructuredToolResult(
98
+ status=ToolResultStatus.ERROR,
99
+ error=f"Network error while opensearch traces fields: {str(e)}",
100
+ params=params,
101
+ )
102
+ except Exception as e:
103
+ logging.warning("Failed to process opensearch traces fields", exc_info=True)
104
+ return StructuredToolResult(
105
+ status=ToolResultStatus.ERROR,
106
+ error=f"Unexpected error: {str(e)}",
107
+ params=params,
108
+ )
109
+
110
+ def get_parameterized_one_liner(self, params) -> str:
111
+ return "list traces documents fields"
112
+
113
+
114
+ class TracesSearchQuery(Tool):
115
+ def __init__(self, toolset: "OpenSearchTracesToolset"):
116
+ super().__init__(
117
+ name="traces_in_range_search",
118
+ description="Get traces in a specified time range for an opensearch query",
119
+ parameters={
120
+ "query": ToolParameter(
121
+ description="An OpenSearch search query. It should be a stringified json, matching opensearch search syntax. "
122
+ "The query must contain a 'range' on the startTimeMillis field. From a given startTimeMillis, until a given startTimeMillis. This is time in milliseconds",
123
+ type="string",
124
+ ),
125
+ },
126
+ )
127
+ self._toolset = toolset
128
+ self._cache = None
129
+
130
+ def _invoke(self, params: Any) -> StructuredToolResult:
131
+ err_msg = ""
132
+ try:
133
+ body = json.loads(params.get("query"))
134
+ full_query = body
135
+ full_query["size"] = int(
136
+ os.environ.get("OPENSEARCH_TRACES_SEARCH_SIZE", "5000")
137
+ )
138
+ logging.debug(f"opensearch traces search query: {full_query}")
139
+ headers = {"Content-Type": "application/json"}
140
+ headers.update(
141
+ add_auth_header(self._toolset.opensearch_config.opensearch_auth_header)
142
+ )
143
+
144
+ logs_response = requests.get(
145
+ url=get_search_url(self._toolset.opensearch_config),
146
+ timeout=180,
147
+ verify=True,
148
+ data=json.dumps(full_query),
149
+ headers=headers,
150
+ )
151
+ if logs_response.status_code > 300:
152
+ err_msg = logs_response.text
153
+
154
+ logs_response.raise_for_status()
155
+ return StructuredToolResult(
156
+ status=ToolResultStatus.SUCCESS,
157
+ data=json.dumps(logs_response.json()),
158
+ params=params,
159
+ )
160
+ except requests.Timeout:
161
+ logging.warning(
162
+ "Timeout while fetching opensearch traces search", exc_info=True
163
+ )
164
+ return StructuredToolResult(
165
+ status=ToolResultStatus.ERROR,
166
+ error=f"Request timed out while fetching opensearch traces search {err_msg}",
167
+ params=params,
168
+ )
169
+ except RequestException as e:
170
+ logging.warning("Failed to fetch opensearch traces search", exc_info=True)
171
+ return StructuredToolResult(
172
+ status=ToolResultStatus.ERROR,
173
+ error=f"Network error while opensearch traces search {err_msg} : {str(e)}",
174
+ params=params,
175
+ )
176
+ except Exception as e:
177
+ logging.warning(
178
+ "Failed to process opensearch traces search ", exc_info=True
179
+ )
180
+ return StructuredToolResult(
181
+ status=ToolResultStatus.ERROR,
182
+ error=f"Unexpected error {err_msg}: {str(e)}",
183
+ params=params,
184
+ )
185
+
186
+ def get_parameterized_one_liner(self, params) -> str:
187
+ return f'search traces: query="{params.get("query")}"'
188
+
189
+
190
+ class OpenSearchTracesToolset(BaseOpenSearchToolset):
191
+ def __init__(self):
192
+ super().__init__(
193
+ name="opensearch/traces",
194
+ description="OpenSearch integration to fetch traces",
195
+ docs_url="https://docs.robusta.dev/master/configuration/holmesgpt/toolsets/opensearch-traces.html",
196
+ icon_url="https://opensearch.org/assets/brand/PNG/Mark/opensearch_mark_default.png",
197
+ prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
198
+ tools=[
199
+ GetTracesFields(toolset=self),
200
+ TracesSearchQuery(toolset=self),
201
+ ],
202
+ tags=[
203
+ ToolsetTag.CORE,
204
+ ],
205
+ )
206
+ template_file_path = os.path.abspath(
207
+ os.path.join(
208
+ os.path.dirname(__file__), "opensearch_traces_instructions.jinja2"
209
+ )
210
+ )
211
+ self._load_llm_instructions(jinja_template=f"file://{template_file_path}")
@@ -0,0 +1,12 @@
1
+ # Traces and Spans
2
+ * All applications are instrumented and has spans and traces.
3
+ * ALWAYS, first, use get_traces_fields to understand the documents fields, then ALWAYS, search for trace documents based on the fields recieved in the previous call. You MUST filter by the fields names that appears here
4
+ * When investigating network errors, latency, or performance issues, ALWAYS check traces and spans
5
+ * The search query should always contain the root 'query' element in the search expression
6
+ * Search traces using the `fetch_opensearch_logs` tool
7
+ * try to search traces based on what you're looking for, and the trace fields available
8
+ * Always build the query with '.keyword' when doing exact match
9
+ * if time range is not specified, get logs for the last hour
10
+ * When investigating numeric errors, these are error codes
11
+ * Always search for traces BEFORE checking logs. If the logs contain information that can lead to another trace search, do another trace search
12
+ * ALWAYS check for traces to learn about the traffic, latency and errors