holmesgpt 0.13.2__py3-none-any.whl → 0.16.2a0__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.
Files changed (134) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +17 -4
  3. holmes/common/env_vars.py +40 -1
  4. holmes/config.py +114 -144
  5. holmes/core/conversations.py +53 -14
  6. holmes/core/feedback.py +191 -0
  7. holmes/core/investigation.py +18 -22
  8. holmes/core/llm.py +489 -88
  9. holmes/core/models.py +103 -1
  10. holmes/core/openai_formatting.py +13 -0
  11. holmes/core/prompt.py +1 -1
  12. holmes/core/safeguards.py +4 -4
  13. holmes/core/supabase_dal.py +293 -100
  14. holmes/core/tool_calling_llm.py +423 -323
  15. holmes/core/tools.py +311 -33
  16. holmes/core/tools_utils/token_counting.py +14 -0
  17. holmes/core/tools_utils/tool_context_window_limiter.py +57 -0
  18. holmes/core/tools_utils/tool_executor.py +13 -8
  19. holmes/core/toolset_manager.py +155 -4
  20. holmes/core/tracing.py +6 -1
  21. holmes/core/transformers/__init__.py +23 -0
  22. holmes/core/transformers/base.py +62 -0
  23. holmes/core/transformers/llm_summarize.py +174 -0
  24. holmes/core/transformers/registry.py +122 -0
  25. holmes/core/transformers/transformer.py +31 -0
  26. holmes/core/truncation/compaction.py +59 -0
  27. holmes/core/truncation/dal_truncation_utils.py +23 -0
  28. holmes/core/truncation/input_context_window_limiter.py +218 -0
  29. holmes/interactive.py +177 -24
  30. holmes/main.py +7 -4
  31. holmes/plugins/prompts/_fetch_logs.jinja2 +26 -1
  32. holmes/plugins/prompts/_general_instructions.jinja2 +1 -2
  33. holmes/plugins/prompts/_runbook_instructions.jinja2 +23 -12
  34. holmes/plugins/prompts/conversation_history_compaction.jinja2 +88 -0
  35. holmes/plugins/prompts/generic_ask.jinja2 +2 -4
  36. holmes/plugins/prompts/generic_ask_conversation.jinja2 +2 -1
  37. holmes/plugins/prompts/generic_ask_for_issue_conversation.jinja2 +2 -1
  38. holmes/plugins/prompts/generic_investigation.jinja2 +2 -1
  39. holmes/plugins/prompts/investigation_procedure.jinja2 +48 -0
  40. holmes/plugins/prompts/kubernetes_workload_ask.jinja2 +2 -1
  41. holmes/plugins/prompts/kubernetes_workload_chat.jinja2 +2 -1
  42. holmes/plugins/runbooks/__init__.py +117 -18
  43. holmes/plugins/runbooks/catalog.json +2 -0
  44. holmes/plugins/toolsets/__init__.py +21 -8
  45. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  46. holmes/plugins/toolsets/aks.yaml +64 -0
  47. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +26 -36
  48. holmes/plugins/toolsets/azure_sql/azure_sql_toolset.py +0 -1
  49. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +10 -7
  50. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +9 -6
  51. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +8 -6
  52. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +8 -6
  53. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +9 -6
  54. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +9 -7
  55. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +9 -6
  56. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +9 -6
  57. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +9 -6
  58. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +9 -6
  59. holmes/plugins/toolsets/bash/bash_toolset.py +10 -13
  60. holmes/plugins/toolsets/bash/common/bash.py +7 -7
  61. holmes/plugins/toolsets/cilium.yaml +284 -0
  62. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
  63. holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
  64. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
  65. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +349 -216
  66. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +190 -19
  67. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +101 -44
  68. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +13 -16
  69. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +25 -31
  70. holmes/plugins/toolsets/git.py +51 -46
  71. holmes/plugins/toolsets/grafana/common.py +15 -3
  72. holmes/plugins/toolsets/grafana/grafana_api.py +46 -24
  73. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +454 -0
  74. holmes/plugins/toolsets/grafana/loki/instructions.jinja2 +9 -0
  75. holmes/plugins/toolsets/grafana/loki/toolset_grafana_loki.py +117 -0
  76. holmes/plugins/toolsets/grafana/toolset_grafana.py +211 -91
  77. holmes/plugins/toolsets/grafana/toolset_grafana_dashboard.jinja2 +27 -0
  78. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
  79. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +653 -293
  80. holmes/plugins/toolsets/grafana/trace_parser.py +1 -1
  81. holmes/plugins/toolsets/internet/internet.py +6 -7
  82. holmes/plugins/toolsets/internet/notion.py +5 -6
  83. holmes/plugins/toolsets/investigator/core_investigation.py +42 -34
  84. holmes/plugins/toolsets/kafka.py +25 -36
  85. holmes/plugins/toolsets/kubernetes.yaml +58 -84
  86. holmes/plugins/toolsets/kubernetes_logs.py +6 -6
  87. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  88. holmes/plugins/toolsets/logging_utils/logging_api.py +80 -4
  89. holmes/plugins/toolsets/mcp/toolset_mcp.py +181 -55
  90. holmes/plugins/toolsets/newrelic/__init__.py +0 -0
  91. holmes/plugins/toolsets/newrelic/new_relic_api.py +125 -0
  92. holmes/plugins/toolsets/newrelic/newrelic.jinja2 +41 -0
  93. holmes/plugins/toolsets/newrelic/newrelic.py +163 -0
  94. holmes/plugins/toolsets/opensearch/opensearch.py +10 -17
  95. holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
  96. holmes/plugins/toolsets/opensearch/opensearch_ppl_query_docs.jinja2 +1616 -0
  97. holmes/plugins/toolsets/opensearch/opensearch_query_assist.py +78 -0
  98. holmes/plugins/toolsets/opensearch/opensearch_query_assist_instructions.jinja2 +223 -0
  99. holmes/plugins/toolsets/opensearch/opensearch_traces.py +13 -16
  100. holmes/plugins/toolsets/openshift.yaml +283 -0
  101. holmes/plugins/toolsets/prometheus/prometheus.py +915 -390
  102. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +43 -2
  103. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  104. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +9 -10
  105. holmes/plugins/toolsets/robusta/robusta.py +236 -65
  106. holmes/plugins/toolsets/robusta/robusta_instructions.jinja2 +26 -9
  107. holmes/plugins/toolsets/runbook/runbook_fetcher.py +137 -26
  108. holmes/plugins/toolsets/service_discovery.py +1 -1
  109. holmes/plugins/toolsets/servicenow_tables/instructions.jinja2 +83 -0
  110. holmes/plugins/toolsets/servicenow_tables/servicenow_tables.py +426 -0
  111. holmes/plugins/toolsets/utils.py +88 -0
  112. holmes/utils/config_utils.py +91 -0
  113. holmes/utils/default_toolset_installation_guide.jinja2 +1 -22
  114. holmes/utils/env.py +7 -0
  115. holmes/utils/global_instructions.py +75 -10
  116. holmes/utils/holmes_status.py +2 -1
  117. holmes/utils/holmes_sync_toolsets.py +0 -2
  118. holmes/utils/krr_utils.py +188 -0
  119. holmes/utils/sentry_helper.py +41 -0
  120. holmes/utils/stream.py +61 -7
  121. holmes/version.py +34 -14
  122. holmesgpt-0.16.2a0.dist-info/LICENSE +178 -0
  123. {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/METADATA +29 -27
  124. {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/RECORD +126 -102
  125. holmes/core/performance_timing.py +0 -72
  126. holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
  127. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +0 -110
  128. holmes/plugins/toolsets/newrelic.py +0 -231
  129. holmes/plugins/toolsets/servicenow/install.md +0 -37
  130. holmes/plugins/toolsets/servicenow/instructions.jinja2 +0 -3
  131. holmes/plugins/toolsets/servicenow/servicenow.py +0 -219
  132. holmesgpt-0.13.2.dist-info/LICENSE.txt +0 -21
  133. {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/WHEEL +0 -0
  134. {holmesgpt-0.13.2.dist-info → holmesgpt-0.16.2a0.dist-info}/entry_points.txt +0 -0
@@ -1,56 +1,36 @@
1
1
  import os
2
- import re
3
- from typing import Any, Dict, List, cast
2
+ from typing import Any, Dict, Tuple, cast, List
4
3
 
5
- import requests # type: ignore
6
4
  import yaml # type: ignore
7
- from pydantic import BaseModel
8
5
 
9
- from holmes.common.env_vars import load_bool
6
+ from holmes.common.env_vars import load_bool, MAX_GRAPH_POINTS
10
7
  from holmes.core.tools import (
11
8
  StructuredToolResult,
12
9
  Tool,
10
+ ToolInvokeContext,
13
11
  ToolParameter,
14
- ToolResultStatus,
12
+ StructuredToolResultStatus,
15
13
  )
14
+ from holmes.plugins.toolsets.consts import STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION
16
15
  from holmes.plugins.toolsets.grafana.base_grafana_toolset import BaseGrafanaToolset
17
16
  from holmes.plugins.toolsets.grafana.common import (
18
- GrafanaConfig,
19
- build_headers,
20
- get_base_url,
17
+ GrafanaTempoConfig,
21
18
  )
22
- from holmes.plugins.toolsets.grafana.tempo_api import (
23
- query_tempo_trace_by_id,
24
- query_tempo_traces,
25
- )
26
- from holmes.plugins.toolsets.grafana.trace_parser import format_traces_list
19
+ from holmes.plugins.toolsets.grafana.grafana_tempo_api import GrafanaTempoAPI
27
20
  from holmes.plugins.toolsets.logging_utils.logging_api import (
28
- DEFAULT_TIME_SPAN_SECONDS,
21
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS,
29
22
  )
30
23
  from holmes.plugins.toolsets.utils import (
31
- get_param_or_raise,
32
- process_timestamps_to_int,
33
24
  toolset_name_for_one_liner,
25
+ process_timestamps_to_int,
26
+ standard_start_datetime_tool_param_description,
27
+ adjust_step_for_max_points,
28
+ seconds_to_duration_string,
29
+ duration_string_to_seconds,
34
30
  )
35
31
 
36
32
  TEMPO_LABELS_ADD_PREFIX = load_bool("TEMPO_LABELS_ADD_PREFIX", True)
37
33
 
38
- ONE_HOUR_IN_SECONDS = 3600
39
- DEFAULT_TRACES_TIME_SPAN_SECONDS = DEFAULT_TIME_SPAN_SECONDS # 7 days
40
- DEFAULT_TAGS_TIME_SPAN_SECONDS = 8 * ONE_HOUR_IN_SECONDS # 8 hours
41
-
42
-
43
- class GrafanaTempoLabelsConfig(BaseModel):
44
- pod: str = "k8s.pod.name"
45
- namespace: str = "k8s.namespace.name"
46
- deployment: str = "k8s.deployment.name"
47
- node: str = "k8s.node.name"
48
- service: str = "service.name"
49
-
50
-
51
- class GrafanaTempoConfig(GrafanaConfig):
52
- labels: GrafanaTempoLabelsConfig = GrafanaTempoLabelsConfig()
53
-
54
34
 
55
35
  class BaseGrafanaTempoToolset(BaseGrafanaToolset):
56
36
  config_class = GrafanaTempoConfig
@@ -67,6 +47,23 @@ class BaseGrafanaTempoToolset(BaseGrafanaToolset):
67
47
  def grafana_config(self) -> GrafanaTempoConfig:
68
48
  return cast(GrafanaTempoConfig, self._grafana_config)
69
49
 
50
+ def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
51
+ """Check Tempo connectivity using the echo endpoint."""
52
+ # First call parent to validate config
53
+ success, msg = super().prerequisites_callable(config)
54
+ if not success:
55
+ return success, msg
56
+
57
+ # Then check Tempo-specific echo endpoint
58
+ try:
59
+ api = GrafanaTempoAPI(self.grafana_config)
60
+ if api.query_echo_endpoint():
61
+ return True, "Successfully connected to Tempo"
62
+ else:
63
+ return False, "Failed to connect to Tempo echo endpoint"
64
+ except Exception as e:
65
+ return False, f"Failed to connect to Tempo: {str(e)}"
66
+
70
67
  def build_k8s_filters(
71
68
  self, params: Dict[str, Any], use_exact_match: bool
72
69
  ) -> List[str]:
@@ -107,228 +104,25 @@ class BaseGrafanaTempoToolset(BaseGrafanaToolset):
107
104
  escaped_value = value.replace('"', '\\"')
108
105
  filters.append(f'{prefix}{label}="{escaped_value}"')
109
106
  else:
110
- # Escape regex special characters for partial match
111
- escaped_value = re.escape(value)
112
- filters.append(f'{prefix}{label}=~".*{escaped_value}.*"')
107
+ # For partial match, use simple substring matching
108
+ # Don't escape anything - let Tempo handle the regex
109
+ filters.append(f'{prefix}{label}=~".*{value}.*"')
113
110
 
114
111
  return filters
115
112
 
116
-
117
- def validate_params(params: Dict[str, Any], expected_params: List[str]):
118
- for param in expected_params:
119
- if param in params and params[param] not in (None, "", [], {}):
120
- return None
121
-
122
- return f"At least one of the following argument is expected but none were set: {expected_params}"
123
-
124
-
125
- class GetTempoTraces(Tool):
126
- def __init__(self, toolset: BaseGrafanaTempoToolset):
127
- super().__init__(
128
- name="fetch_tempo_traces",
129
- description="""Lists Tempo traces. At least one of `service_name`, `pod_name` or `deployment_name` argument is required.""",
130
- parameters={
131
- "min_duration": ToolParameter(
132
- description="The minimum duration of traces to fetch, e.g., '5s' for 5 seconds.",
133
- type="string",
134
- required=True,
135
- ),
136
- "service_name": ToolParameter(
137
- description="Filter traces by service name",
138
- type="string",
139
- required=False,
140
- ),
141
- "pod_name": ToolParameter(
142
- description="Filter traces by pod name",
143
- type="string",
144
- required=False,
145
- ),
146
- "namespace_name": ToolParameter(
147
- description="Filter traces by namespace",
148
- type="string",
149
- required=False,
150
- ),
151
- "deployment_name": ToolParameter(
152
- description="Filter traces by deployment name",
153
- type="string",
154
- required=False,
155
- ),
156
- "node_name": ToolParameter(
157
- description="Filter traces by node",
158
- type="string",
159
- required=False,
160
- ),
161
- "start_datetime": ToolParameter(
162
- description=f"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 -{DEFAULT_TRACES_TIME_SPAN_SECONDS}",
163
- type="string",
164
- required=False,
165
- ),
166
- "end_datetime": ToolParameter(
167
- description="The ending time boundary for the trace search period. String in RFC3339 format. Defaults to NOW().",
168
- type="string",
169
- required=False,
170
- ),
171
- "limit": ToolParameter(
172
- description="Maximum number of traces to return. Defaults to 50",
173
- type="string",
174
- required=False,
175
- ),
176
- "sort": ToolParameter(
177
- description="One of 'descending', 'ascending' or 'none' for no sorting. Defaults to descending",
178
- type="string",
179
- required=False,
180
- ),
181
- },
182
- )
183
- self._toolset = toolset
184
-
185
- def _invoke(
186
- self, params: dict, user_approved: bool = False
187
- ) -> StructuredToolResult:
188
- api_key = self._toolset.grafana_config.api_key
189
- headers = self._toolset.grafana_config.headers
190
-
191
- invalid_params_error = validate_params(
192
- params, ["service_name", "pod_name", "deployment_name"]
193
- )
194
- if invalid_params_error:
195
- return StructuredToolResult(
196
- status=ToolResultStatus.ERROR,
197
- error=invalid_params_error,
198
- params=params,
199
- )
200
-
201
- start, end = process_timestamps_to_int(
202
- params.get("start_datetime"),
203
- params.get("end_datetime"),
204
- default_time_span_seconds=DEFAULT_TRACES_TIME_SPAN_SECONDS,
205
- )
206
-
207
- filters = self._toolset.build_k8s_filters(params, use_exact_match=True)
208
-
209
- filters.append(f'duration>{get_param_or_raise(params, "min_duration")}')
210
-
211
- query = " && ".join(filters)
212
- query = f"{{{query}}}"
213
-
214
- base_url = get_base_url(self._toolset.grafana_config)
215
- traces = query_tempo_traces(
216
- base_url=base_url,
217
- api_key=api_key,
218
- headers=headers,
219
- query=query,
220
- start=start,
221
- end=end,
222
- limit=params.get("limit", 50),
223
- )
224
- return StructuredToolResult(
225
- status=ToolResultStatus.SUCCESS,
226
- data=format_traces_list(traces),
227
- params=params,
228
- invocation=query,
229
- )
230
-
231
- def get_parameterized_one_liner(self, params: Dict) -> str:
232
- return f"{toolset_name_for_one_liner(self._toolset.name)}: Fetched Tempo Traces (min_duration={params.get('min_duration')})"
233
-
234
-
235
- class GetTempoTags(Tool):
236
- def __init__(self, toolset: BaseGrafanaTempoToolset):
237
- super().__init__(
238
- name="fetch_tempo_tags",
239
- description="List the tags available in Tempo",
240
- parameters={
241
- "start_datetime": ToolParameter(
242
- description=f"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 -{DEFAULT_TAGS_TIME_SPAN_SECONDS}",
243
- type="string",
244
- required=False,
245
- ),
246
- "end_datetime": ToolParameter(
247
- description="The ending time boundary for the search period. String in RFC3339 format. Defaults to NOW().",
248
- type="string",
249
- required=False,
250
- ),
251
- },
252
- )
253
- self._toolset = toolset
254
-
255
- def _invoke(
256
- self, params: dict, user_approved: bool = False
257
- ) -> StructuredToolResult:
258
- api_key = self._toolset.grafana_config.api_key
259
- headers = self._toolset.grafana_config.headers
260
- start, end = process_timestamps_to_int(
261
- start=params.get("start_datetime"),
262
- end=params.get("end_datetime"),
263
- default_time_span_seconds=DEFAULT_TAGS_TIME_SPAN_SECONDS,
264
- )
265
-
266
- base_url = get_base_url(self._toolset.grafana_config)
267
- url = f"{base_url}/api/v2/search/tags?start={start}&end={end}"
268
-
269
- try:
270
- response = requests.get(
271
- url,
272
- headers=build_headers(api_key=api_key, additional_headers=headers),
273
- timeout=60,
274
- )
275
- response.raise_for_status() # Raise an error for non-2xx responses
276
- data = response.json()
277
- return StructuredToolResult(
278
- status=ToolResultStatus.SUCCESS,
279
- data=yaml.dump(data.get("scopes")),
280
- params=params,
281
- )
282
- except requests.exceptions.RequestException as e:
283
- raise Exception(f"Failed to retrieve tags: {e} \n for URL: {url}")
284
-
285
- def get_parameterized_one_liner(self, params: Dict) -> str:
286
- return f"{toolset_name_for_one_liner(self._toolset.name)}: Fetched Tempo tags"
287
-
288
-
289
- class GetTempoTraceById(Tool):
290
- def __init__(self, toolset: BaseGrafanaTempoToolset):
291
- super().__init__(
292
- name="fetch_tempo_trace_by_id",
293
- description="""Retrieves detailed information about a Tempo trace using its trace ID. Use this to investigate a trace.""",
294
- parameters={
295
- "trace_id": ToolParameter(
296
- description="The unique trace ID to fetch.",
297
- type="string",
298
- required=True,
299
- ),
300
- },
301
- )
302
- self._toolset = toolset
303
-
304
- def _invoke(
305
- self, params: dict, user_approved: bool = False
306
- ) -> StructuredToolResult:
307
- labels_mapping = self._toolset.grafana_config.labels
308
- labels = list(labels_mapping.model_dump().values())
309
-
310
- base_url = get_base_url(self._toolset.grafana_config)
311
- trace_data = query_tempo_trace_by_id(
312
- base_url=base_url,
313
- api_key=self._toolset.grafana_config.api_key,
314
- headers=self._toolset.grafana_config.headers,
315
- trace_id=get_param_or_raise(params, "trace_id"),
316
- key_labels=labels,
113
+ @staticmethod
114
+ def adjust_start_end_time(params: Dict) -> Tuple[int, int]:
115
+ return process_timestamps_to_int(
116
+ start=params.get("start"),
117
+ end=params.get("end"),
118
+ default_time_span_seconds=DEFAULT_GRAPH_TIME_SPAN_SECONDS,
317
119
  )
318
- return StructuredToolResult(
319
- status=ToolResultStatus.SUCCESS,
320
- data=trace_data,
321
- params=params,
322
- )
323
-
324
- def get_parameterized_one_liner(self, params: Dict) -> str:
325
- return f"{toolset_name_for_one_liner(self._toolset.name)}: Fetched Tempo Trace (trace_id={params.get('trace_id')})"
326
120
 
327
121
 
328
122
  class FetchTracesSimpleComparison(Tool):
329
123
  def __init__(self, toolset: BaseGrafanaTempoToolset):
330
124
  super().__init__(
331
- name="fetch_tempo_traces_comparative_sample",
125
+ name="tempo_fetch_traces_comparative_sample",
332
126
  description="""Fetches statistics and representative samples of fast, slow, and typical traces for performance analysis. Requires either a `base_query` OR at least one of `service_name`, `pod_name`, `namespace_name`, `deployment_name`, `node_name`.
333
127
 
334
128
  Important: call this tool first when investigating performance issues via traces. This tool provides comprehensive analysis for identifying patterns.
@@ -364,7 +158,11 @@ Examples:
364
158
  required=False,
365
159
  ),
366
160
  "base_query": ToolParameter(
367
- description="Custom TraceQL filter",
161
+ description=(
162
+ "Custom TraceQL filter. Supports span/resource attributes, "
163
+ "duration, and aggregates (count(), avg(), min(), max(), sum()). "
164
+ "Examples: '{span.http.status_code>=400}', '{duration>100ms}'"
165
+ ),
368
166
  type="string",
369
167
  required=False,
370
168
  ),
@@ -373,13 +171,15 @@ Examples:
373
171
  type="integer",
374
172
  required=False,
375
173
  ),
376
- "start_datetime": ToolParameter(
377
- description=f"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 -{DEFAULT_TRACES_TIME_SPAN_SECONDS}",
174
+ "start": ToolParameter(
175
+ description=standard_start_datetime_tool_param_description(
176
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
177
+ ),
378
178
  type="string",
379
179
  required=False,
380
180
  ),
381
- "end_datetime": ToolParameter(
382
- description="The ending time boundary for the trace search period. String in RFC3339 format. Defaults to NOW().",
181
+ "end": ToolParameter(
182
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
383
183
  type="string",
384
184
  required=False,
385
185
  ),
@@ -387,9 +187,15 @@ Examples:
387
187
  )
388
188
  self._toolset = toolset
389
189
 
390
- def _invoke(
391
- self, params: dict, user_approved: bool = False
392
- ) -> StructuredToolResult:
190
+ @staticmethod
191
+ def validate_params(params: Dict[str, Any], expected_params: List[str]):
192
+ for param in expected_params:
193
+ if param in params and params[param] not in (None, "", [], {}):
194
+ return None
195
+
196
+ return f"At least one of the following argument is expected but none were set: {expected_params}"
197
+
198
+ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult:
393
199
  try:
394
200
  # Build query
395
201
  if params.get("base_query"):
@@ -399,7 +205,7 @@ Examples:
399
205
  filters = self._toolset.build_k8s_filters(params, use_exact_match=False)
400
206
 
401
207
  # Validate that at least one parameter was provided
402
- invalid_params_error = validate_params(
208
+ invalid_params_error = FetchTracesSimpleComparison.validate_params(
403
209
  params,
404
210
  [
405
211
  "service_name",
@@ -411,7 +217,7 @@ Examples:
411
217
  )
412
218
  if invalid_params_error:
413
219
  return StructuredToolResult(
414
- status=ToolResultStatus.ERROR,
220
+ status=StructuredToolResultStatus.ERROR,
415
221
  error=invalid_params_error,
416
222
  params=params,
417
223
  )
@@ -420,30 +226,35 @@ Examples:
420
226
 
421
227
  sample_count = params.get("sample_count", 3)
422
228
 
423
- start, end = process_timestamps_to_int(
424
- params.get("start_datetime"),
425
- params.get("end_datetime"),
426
- default_time_span_seconds=DEFAULT_TRACES_TIME_SPAN_SECONDS,
427
- )
229
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
428
230
 
429
- base_url = get_base_url(self._toolset.grafana_config)
231
+ # Create API instance
232
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
430
233
 
431
234
  # Step 1: Get all trace summaries
432
235
  stats_query = f"{{{base_query}}}"
433
- all_traces_response = query_tempo_traces(
434
- base_url=base_url,
435
- api_key=self._toolset.grafana_config.api_key,
436
- headers=self._toolset.grafana_config.headers,
437
- query=stats_query,
236
+
237
+ # Debug log the query (useful for troubleshooting)
238
+ import logging
239
+
240
+ logger = logging.getLogger(__name__)
241
+ logger.debug(f"Tempo query: {stats_query}")
242
+
243
+ logger.debug(f"start: {start}, end: {end}")
244
+
245
+ all_traces_response = api.search_traces_by_query(
246
+ q=stats_query,
438
247
  start=start,
439
248
  end=end,
440
249
  limit=1000,
441
250
  )
442
251
 
252
+ logger.debug(f"Response: {all_traces_response}")
253
+
443
254
  traces = all_traces_response.get("traces", [])
444
255
  if not traces:
445
256
  return StructuredToolResult(
446
- status=ToolResultStatus.SUCCESS,
257
+ status=StructuredToolResultStatus.SUCCESS,
447
258
  data="No traces found matching the query",
448
259
  params=params,
449
260
  )
@@ -488,44 +299,33 @@ Examples:
488
299
  return None
489
300
 
490
301
  try:
491
- url = f"{base_url}/api/traces/{trace_id}"
492
- response = requests.get(
493
- url,
494
- headers=build_headers(
495
- api_key=self._toolset.grafana_config.api_key,
496
- additional_headers=self._toolset.grafana_config.headers,
497
- ),
498
- timeout=5,
302
+ start_nano = trace_summary.get("startTimeUnixNano")
303
+ trace_start = (
304
+ int(int(start_nano) / 1_000_000_000) if start_nano else None
305
+ )
306
+
307
+ trace_data = api.query_trace_by_id_v2(
308
+ trace_id=trace_id, start=trace_start
499
309
  )
500
- response.raise_for_status()
501
310
  return {
502
311
  "traceID": trace_id,
503
312
  "durationMs": trace_summary.get("durationMs", 0),
504
313
  "rootServiceName": trace_summary.get(
505
314
  "rootServiceName", "unknown"
506
315
  ),
507
- "traceData": response.json(), # Raw trace data
316
+ "traceData": trace_data, # Raw trace data
508
317
  }
509
- except requests.exceptions.RequestException as e:
318
+ except Exception as e:
510
319
  error_msg = f"Failed to fetch full trace: {str(e)}"
511
- if hasattr(e, "response") and e.response is not None:
512
- error_msg += f" (Status: {e.response.status_code})"
513
320
  return {
514
321
  "traceID": trace_id,
515
322
  "durationMs": trace_summary.get("durationMs", 0),
516
323
  "error": error_msg,
517
324
  }
518
- except (ValueError, KeyError) as e:
519
- return {
520
- "traceID": trace_id,
521
- "durationMs": trace_summary.get("durationMs", 0),
522
- "error": f"Failed to parse trace data: {str(e)}",
523
- }
524
325
 
525
326
  # Fetch the selected traces
526
327
  result = {
527
328
  "statistics": stats,
528
- "all_trace_durations_ms": durations, # All durations for distribution analysis
529
329
  "fastest_traces": [
530
330
  fetch_full_trace(sorted_traces[i]) for i in fastest_indices
531
331
  ],
@@ -537,14 +337,14 @@ Examples:
537
337
 
538
338
  # Return as YAML for readability
539
339
  return StructuredToolResult(
540
- status=ToolResultStatus.SUCCESS,
340
+ status=StructuredToolResultStatus.SUCCESS,
541
341
  data=yaml.dump(result, default_flow_style=False, sort_keys=False),
542
342
  params=params,
543
343
  )
544
344
 
545
345
  except Exception as e:
546
346
  return StructuredToolResult(
547
- status=ToolResultStatus.ERROR,
347
+ status=StructuredToolResultStatus.ERROR,
548
348
  error=f"Error fetching traces: {str(e)}",
549
349
  params=params,
550
350
  )
@@ -553,6 +353,562 @@ Examples:
553
353
  return f"{toolset_name_for_one_liner(self._toolset.name)}: Simple Tempo Traces Comparison"
554
354
 
555
355
 
356
+ class SearchTracesByQuery(Tool):
357
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
358
+ super().__init__(
359
+ name="tempo_search_traces_by_query",
360
+ description=(
361
+ "Search for traces using TraceQL query language. "
362
+ "Uses the Tempo API endpoint: GET /api/search with 'q' parameter.\n\n"
363
+ "TraceQL can select traces based on:\n"
364
+ "- Span and resource attributes\n"
365
+ "- Timing and duration\n"
366
+ "- Aggregate functions:\n"
367
+ " • count() - Count number of spans\n"
368
+ " • avg(attribute) - Calculate average\n"
369
+ " • min(attribute) - Find minimum value\n"
370
+ " • max(attribute) - Find maximum value\n"
371
+ " • sum(attribute) - Sum values\n\n"
372
+ "Examples:\n"
373
+ '- Specific operation: {resource.service.name = "frontend" && name = "POST /api/orders"}\n'
374
+ '- Error traces: {resource.service.name="frontend" && name = "POST /api/orders" && status = error}\n'
375
+ '- HTTP errors: {resource.service.name="frontend" && name = "POST /api/orders" && span.http.status_code >= 500}\n'
376
+ '- Multi-service: {span.service.name="frontend" && name = "GET /api/products/{id}"} && {span.db.system="postgresql"}\n'
377
+ "- With aggregates: { status = error } | by(resource.service.name) | count() > 1"
378
+ ),
379
+ parameters={
380
+ "q": ToolParameter(
381
+ description=(
382
+ "TraceQL query. Supports filtering by span/resource attributes, "
383
+ "duration, and aggregate functions (count(), avg(), min(), max(), sum()). "
384
+ "Examples: '{resource.service.name = \"frontend\"}', "
385
+ '\'{resource.service.name="frontend" && name = "POST /api/orders" && status = error}\', '
386
+ '\'{resource.service.name="frontend" && name = "POST /api/orders" && span.http.status_code >= 500}\', '
387
+ "'{} | count() > 10'"
388
+ ),
389
+ type="string",
390
+ required=True,
391
+ ),
392
+ "limit": ToolParameter(
393
+ description="Maximum number of traces to return",
394
+ type="integer",
395
+ required=False,
396
+ ),
397
+ "start": ToolParameter(
398
+ description=standard_start_datetime_tool_param_description(
399
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
400
+ ),
401
+ type="string",
402
+ required=False,
403
+ ),
404
+ "end": ToolParameter(
405
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
406
+ type="string",
407
+ required=False,
408
+ ),
409
+ "spss": ToolParameter(
410
+ description="Spans per span set",
411
+ type="integer",
412
+ required=False,
413
+ ),
414
+ },
415
+ )
416
+ self._toolset = toolset
417
+
418
+ def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
419
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
420
+
421
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
422
+
423
+ try:
424
+ result = api.search_traces_by_query(
425
+ q=params["q"],
426
+ limit=params.get("limit"),
427
+ start=start,
428
+ end=end,
429
+ spss=params.get("spss"),
430
+ )
431
+ return StructuredToolResult(
432
+ status=StructuredToolResultStatus.SUCCESS,
433
+ data=yaml.dump(result, default_flow_style=False),
434
+ params=params,
435
+ )
436
+ except Exception as e:
437
+ return StructuredToolResult(
438
+ status=StructuredToolResultStatus.ERROR,
439
+ error=str(e),
440
+ params=params,
441
+ )
442
+
443
+ def get_parameterized_one_liner(self, params: Dict) -> str:
444
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Searched traces with TraceQL"
445
+
446
+
447
+ class SearchTracesByTags(Tool):
448
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
449
+ super().__init__(
450
+ name="tempo_search_traces_by_tags",
451
+ description=(
452
+ "Search for traces using logfmt-encoded tags. "
453
+ "Uses the Tempo API endpoint: GET /api/search with 'tags' parameter. "
454
+ 'Example: service.name="api" http.status_code="500"'
455
+ ),
456
+ parameters={
457
+ "tags": ToolParameter(
458
+ description='Logfmt-encoded span/process attributes (e.g., \'service.name="api" http.status_code="500"\')',
459
+ type="string",
460
+ required=True,
461
+ ),
462
+ "min_duration": ToolParameter(
463
+ description="Minimum trace duration (e.g., '5s', '100ms')",
464
+ type="string",
465
+ required=False,
466
+ ),
467
+ "max_duration": ToolParameter(
468
+ description="Maximum trace duration (e.g., '10s', '1000ms')",
469
+ type="string",
470
+ required=False,
471
+ ),
472
+ "limit": ToolParameter(
473
+ description="Maximum number of traces to return",
474
+ type="integer",
475
+ required=False,
476
+ ),
477
+ "start": ToolParameter(
478
+ description=standard_start_datetime_tool_param_description(
479
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
480
+ ),
481
+ type="string",
482
+ required=False,
483
+ ),
484
+ "end": ToolParameter(
485
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
486
+ type="string",
487
+ required=False,
488
+ ),
489
+ "spss": ToolParameter(
490
+ description="Spans per span set",
491
+ type="integer",
492
+ required=False,
493
+ ),
494
+ },
495
+ )
496
+ self._toolset = toolset
497
+
498
+ def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
499
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
500
+
501
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
502
+
503
+ try:
504
+ result = api.search_traces_by_tags(
505
+ tags=params["tags"],
506
+ min_duration=params.get("min_duration"),
507
+ max_duration=params.get("max_duration"),
508
+ limit=params.get("limit"),
509
+ start=start,
510
+ end=end,
511
+ spss=params.get("spss"),
512
+ )
513
+ return StructuredToolResult(
514
+ status=StructuredToolResultStatus.SUCCESS,
515
+ data=yaml.dump(result, default_flow_style=False),
516
+ params=params,
517
+ )
518
+ except Exception as e:
519
+ return StructuredToolResult(
520
+ status=StructuredToolResultStatus.ERROR,
521
+ error=str(e),
522
+ params=params,
523
+ )
524
+
525
+ def get_parameterized_one_liner(self, params: Dict) -> str:
526
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Searched traces with tags"
527
+
528
+
529
+ class QueryTraceById(Tool):
530
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
531
+ super().__init__(
532
+ name="tempo_query_trace_by_id",
533
+ description=(
534
+ "Retrieve detailed trace information by trace ID. "
535
+ "Uses the Tempo API endpoint: GET /api/v2/traces/{trace_id}. "
536
+ "Returns the full trace data in OpenTelemetry format."
537
+ ),
538
+ parameters={
539
+ "trace_id": ToolParameter(
540
+ description="The unique trace ID to fetch",
541
+ type="string",
542
+ required=True,
543
+ ),
544
+ "start": ToolParameter(
545
+ description=standard_start_datetime_tool_param_description(
546
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
547
+ ),
548
+ type="string",
549
+ required=False,
550
+ ),
551
+ "end": ToolParameter(
552
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
553
+ type="string",
554
+ required=False,
555
+ ),
556
+ },
557
+ )
558
+ self._toolset = toolset
559
+
560
+ def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
561
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
562
+
563
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
564
+
565
+ try:
566
+ trace_data = api.query_trace_by_id_v2(
567
+ trace_id=params["trace_id"],
568
+ start=start,
569
+ end=end,
570
+ )
571
+
572
+ # Return raw trace data as YAML for readability
573
+ return StructuredToolResult(
574
+ status=StructuredToolResultStatus.SUCCESS,
575
+ data=yaml.dump(trace_data, default_flow_style=False),
576
+ params=params,
577
+ )
578
+ except Exception as e:
579
+ return StructuredToolResult(
580
+ status=StructuredToolResultStatus.ERROR,
581
+ error=str(e),
582
+ params=params,
583
+ )
584
+
585
+ def get_parameterized_one_liner(self, params: Dict) -> str:
586
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Retrieved trace {params.get('trace_id')}"
587
+
588
+
589
+ class SearchTagNames(Tool):
590
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
591
+ super().__init__(
592
+ name="tempo_search_tag_names",
593
+ description=(
594
+ "Discover available tag names across traces. "
595
+ "Uses the Tempo API endpoint: GET /api/v2/search/tags. "
596
+ "Returns tags organized by scope (resource, span, intrinsic)."
597
+ ),
598
+ parameters={
599
+ "scope": ToolParameter(
600
+ description="Filter by scope: 'resource', 'span', or 'intrinsic'",
601
+ type="string",
602
+ required=False,
603
+ ),
604
+ "q": ToolParameter(
605
+ description="TraceQL query to filter tags (e.g., '{resource.cluster=\"us-east-1\"}')",
606
+ type="string",
607
+ required=False,
608
+ ),
609
+ "start": ToolParameter(
610
+ description=standard_start_datetime_tool_param_description(
611
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
612
+ ),
613
+ type="string",
614
+ required=False,
615
+ ),
616
+ "end": ToolParameter(
617
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
618
+ type="string",
619
+ required=False,
620
+ ),
621
+ "limit": ToolParameter(
622
+ description="Maximum number of tag names to return",
623
+ type="integer",
624
+ required=False,
625
+ ),
626
+ "max_stale_values": ToolParameter(
627
+ description="Maximum stale values parameter",
628
+ type="integer",
629
+ required=False,
630
+ ),
631
+ },
632
+ )
633
+ self._toolset = toolset
634
+
635
+ def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
636
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
637
+
638
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
639
+
640
+ try:
641
+ result = api.search_tag_names_v2(
642
+ scope=params.get("scope"),
643
+ q=params.get("q"),
644
+ start=start,
645
+ end=end,
646
+ limit=params.get("limit"),
647
+ max_stale_values=params.get("max_stale_values"),
648
+ )
649
+ return StructuredToolResult(
650
+ status=StructuredToolResultStatus.SUCCESS,
651
+ data=yaml.dump(result, default_flow_style=False),
652
+ params=params,
653
+ )
654
+ except Exception as e:
655
+ return StructuredToolResult(
656
+ status=StructuredToolResultStatus.ERROR,
657
+ error=str(e),
658
+ params=params,
659
+ )
660
+
661
+ def get_parameterized_one_liner(self, params: Dict) -> str:
662
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Discovered tag names"
663
+
664
+
665
+ class SearchTagValues(Tool):
666
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
667
+ super().__init__(
668
+ name="tempo_search_tag_values",
669
+ description=(
670
+ "Get all values for a specific tag. "
671
+ "Uses the Tempo API endpoint: GET /api/v2/search/tag/{tag}/values. "
672
+ "Useful for discovering what values exist for a given tag."
673
+ ),
674
+ parameters={
675
+ "tag": ToolParameter(
676
+ description="The tag name to get values for (e.g., 'resource.service.name', 'http.status_code')",
677
+ type="string",
678
+ required=True,
679
+ ),
680
+ "q": ToolParameter(
681
+ description="TraceQL query to filter tag values (e.g., '{resource.cluster=\"us-east-1\"}')",
682
+ type="string",
683
+ required=False,
684
+ ),
685
+ "start": ToolParameter(
686
+ description=standard_start_datetime_tool_param_description(
687
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
688
+ ),
689
+ type="string",
690
+ required=False,
691
+ ),
692
+ "end": ToolParameter(
693
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
694
+ type="string",
695
+ required=False,
696
+ ),
697
+ "limit": ToolParameter(
698
+ description="Maximum number of values to return",
699
+ type="integer",
700
+ required=False,
701
+ ),
702
+ "max_stale_values": ToolParameter(
703
+ description="Maximum stale values parameter",
704
+ type="integer",
705
+ required=False,
706
+ ),
707
+ },
708
+ )
709
+ self._toolset = toolset
710
+
711
+ def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
712
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
713
+
714
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
715
+
716
+ try:
717
+ result = api.search_tag_values_v2(
718
+ tag=params["tag"],
719
+ q=params.get("q"),
720
+ start=start,
721
+ end=end,
722
+ limit=params.get("limit"),
723
+ max_stale_values=params.get("max_stale_values"),
724
+ )
725
+ return StructuredToolResult(
726
+ status=StructuredToolResultStatus.SUCCESS,
727
+ data=yaml.dump(result, default_flow_style=False),
728
+ params=params,
729
+ )
730
+ except Exception as e:
731
+ return StructuredToolResult(
732
+ status=StructuredToolResultStatus.ERROR,
733
+ error=str(e),
734
+ params=params,
735
+ )
736
+
737
+ def get_parameterized_one_liner(self, params: Dict) -> str:
738
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Retrieved values for tag '{params.get('tag')}'"
739
+
740
+
741
+ class QueryMetricsInstant(Tool):
742
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
743
+ super().__init__(
744
+ name="tempo_query_metrics_instant",
745
+ description=(
746
+ "Compute a single TraceQL metric value across time range. "
747
+ "Uses the Tempo API endpoint: GET /api/metrics/query. "
748
+ "TraceQL metrics compute aggregated metrics from trace data. "
749
+ "Returns a single value for the entire time range. "
750
+ "Basic syntax: {selector} | function(attribute) [by (grouping)]\n\n"
751
+ "TraceQL metrics can help answer questions like:\n"
752
+ "- How many database calls across all systems are downstream of your application?\n"
753
+ "- What services beneath a given endpoint are failing?\n"
754
+ "- What services beneath an endpoint are slow?\n\n"
755
+ "TraceQL metrics help you answer these questions by parsing your traces in aggregate. "
756
+ "The instant version returns a single value for the query and is preferred over "
757
+ "query_metrics_range when you don't need the granularity of a full time-series but want "
758
+ "a total sum or single value computed across the whole time range."
759
+ ),
760
+ parameters={
761
+ "q": ToolParameter(
762
+ description=(
763
+ "TraceQL metrics query. Supported functions: rate, count_over_time, "
764
+ "sum_over_time, max_over_time, min_over_time, avg_over_time, "
765
+ "quantile_over_time, histogram_over_time, compare. "
766
+ "Can use topk or bottomk modifiers. "
767
+ "Syntax: {selector} | function(attribute) [by (grouping)]. "
768
+ 'Example: {resource.service.name="api"} | avg_over_time(duration)'
769
+ ),
770
+ type="string",
771
+ required=True,
772
+ ),
773
+ "start": ToolParameter(
774
+ description=standard_start_datetime_tool_param_description(
775
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
776
+ ),
777
+ type="string",
778
+ required=False,
779
+ ),
780
+ "end": ToolParameter(
781
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
782
+ type="string",
783
+ required=False,
784
+ ),
785
+ },
786
+ )
787
+ self._toolset = toolset
788
+
789
+ def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
790
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
791
+
792
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
793
+
794
+ try:
795
+ result = api.query_metrics_instant(
796
+ q=params["q"],
797
+ start=start,
798
+ end=end,
799
+ )
800
+ return StructuredToolResult(
801
+ status=StructuredToolResultStatus.SUCCESS,
802
+ data=yaml.dump(result, default_flow_style=False),
803
+ params=params,
804
+ )
805
+ except Exception as e:
806
+ return StructuredToolResult(
807
+ status=StructuredToolResultStatus.ERROR,
808
+ error=str(e),
809
+ params=params,
810
+ )
811
+
812
+ def get_parameterized_one_liner(self, params: Dict) -> str:
813
+ return (
814
+ f"{toolset_name_for_one_liner(self._toolset.name)}: Computed TraceQL metric"
815
+ )
816
+
817
+
818
+ class QueryMetricsRange(Tool):
819
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
820
+ super().__init__(
821
+ name="tempo_query_metrics_range",
822
+ description=(
823
+ "Get time series data from TraceQL metrics queries. "
824
+ "Uses the Tempo API endpoint: GET /api/metrics/query_range. "
825
+ "Returns metrics computed at regular intervals (controlled by 'step' parameter). "
826
+ "Use this for graphing metrics over time or analyzing trends. "
827
+ "Basic syntax: {selector} | function(attribute) [by (grouping)]\n\n"
828
+ "TraceQL metrics can help answer questions like:\n"
829
+ "- How many database calls across all systems are downstream of your application?\n"
830
+ "- What services beneath a given endpoint are failing?\n"
831
+ "- What services beneath an endpoint are slow?\n\n"
832
+ "TraceQL metrics help you answer these questions by parsing your traces in aggregate."
833
+ ),
834
+ parameters={
835
+ "q": ToolParameter(
836
+ description=(
837
+ "TraceQL metrics query. Supported functions: rate, count_over_time, "
838
+ "sum_over_time, max_over_time, min_over_time, avg_over_time, "
839
+ "quantile_over_time, histogram_over_time, compare. "
840
+ "Can use topk or bottomk modifiers. "
841
+ "Syntax: {selector} | function(attribute) [by (grouping)]. "
842
+ 'Example: {resource.service.name="api"} | avg_over_time(duration)'
843
+ ),
844
+ type="string",
845
+ required=True,
846
+ ),
847
+ "step": ToolParameter(
848
+ description="Time series granularity (e.g., '1m', '5m', '1h')",
849
+ type="string",
850
+ required=False,
851
+ ),
852
+ "start": ToolParameter(
853
+ description=standard_start_datetime_tool_param_description(
854
+ DEFAULT_GRAPH_TIME_SPAN_SECONDS
855
+ ),
856
+ type="string",
857
+ required=False,
858
+ ),
859
+ "end": ToolParameter(
860
+ description=STANDARD_END_DATETIME_TOOL_PARAM_DESCRIPTION,
861
+ type="string",
862
+ required=False,
863
+ ),
864
+ "exemplars": ToolParameter(
865
+ description="Maximum number of exemplars to return",
866
+ type="integer",
867
+ required=False,
868
+ ),
869
+ },
870
+ )
871
+ self._toolset = toolset
872
+
873
+ def _invoke(self, params: Dict, context: ToolInvokeContext) -> StructuredToolResult:
874
+ api = GrafanaTempoAPI(self._toolset.grafana_config)
875
+
876
+ start, end = BaseGrafanaTempoToolset.adjust_start_end_time(params)
877
+
878
+ # Calculate appropriate step
879
+ step_param = params.get("step")
880
+ step_seconds = duration_string_to_seconds(step_param) if step_param else None
881
+ adjusted_step = adjust_step_for_max_points(
882
+ end - start,
883
+ int(MAX_GRAPH_POINTS),
884
+ step_seconds,
885
+ )
886
+ step = seconds_to_duration_string(adjusted_step)
887
+
888
+ try:
889
+ result = api.query_metrics_range(
890
+ q=params["q"],
891
+ step=step,
892
+ start=start,
893
+ end=end,
894
+ exemplars=params.get("exemplars"),
895
+ )
896
+ return StructuredToolResult(
897
+ status=StructuredToolResultStatus.SUCCESS,
898
+ data=yaml.dump(result, default_flow_style=False),
899
+ params=params,
900
+ )
901
+ except Exception as e:
902
+ return StructuredToolResult(
903
+ status=StructuredToolResultStatus.ERROR,
904
+ error=str(e),
905
+ params=params,
906
+ )
907
+
908
+ def get_parameterized_one_liner(self, params: Dict) -> str:
909
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Retrieved TraceQL metrics time series"
910
+
911
+
556
912
  class GrafanaTempoToolset(BaseGrafanaTempoToolset):
557
913
  def __init__(self):
558
914
  super().__init__(
@@ -562,9 +918,13 @@ class GrafanaTempoToolset(BaseGrafanaTempoToolset):
562
918
  docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanatempo/",
563
919
  tools=[
564
920
  FetchTracesSimpleComparison(self),
565
- GetTempoTraces(self),
566
- GetTempoTraceById(self),
567
- GetTempoTags(self),
921
+ SearchTracesByQuery(self),
922
+ SearchTracesByTags(self),
923
+ QueryTraceById(self),
924
+ SearchTagNames(self),
925
+ SearchTagValues(self),
926
+ QueryMetricsInstant(self),
927
+ QueryMetricsRange(self),
568
928
  ],
569
929
  )
570
930
  template_file_path = os.path.abspath(