holmesgpt 0.13.3__py3-none-any.whl → 0.14.0a0__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.

@@ -1,10 +1,7 @@
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
6
  from holmes.common.env_vars import load_bool
10
7
  from holmes.core.tools import (
@@ -15,43 +12,25 @@ from holmes.core.tools import (
15
12
  )
16
13
  from holmes.plugins.toolsets.grafana.base_grafana_toolset import BaseGrafanaToolset
17
14
  from holmes.plugins.toolsets.grafana.common import (
18
- GrafanaConfig,
19
- build_headers,
20
- get_base_url,
15
+ GrafanaTempoConfig,
21
16
  )
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
17
+ from holmes.plugins.toolsets.grafana.grafana_tempo_api import GrafanaTempoAPI
27
18
  from holmes.plugins.toolsets.logging_utils.logging_api import (
28
19
  DEFAULT_TIME_SPAN_SECONDS,
29
20
  )
30
21
  from holmes.plugins.toolsets.utils import (
31
- get_param_or_raise,
32
- process_timestamps_to_int,
33
22
  toolset_name_for_one_liner,
23
+ process_timestamps_to_int,
34
24
  )
35
25
 
36
26
  TEMPO_LABELS_ADD_PREFIX = load_bool("TEMPO_LABELS_ADD_PREFIX", True)
27
+ TEMPO_API_USE_POST = False # Use GET method for direct API mapping
37
28
 
38
29
  ONE_HOUR_IN_SECONDS = 3600
39
30
  DEFAULT_TRACES_TIME_SPAN_SECONDS = DEFAULT_TIME_SPAN_SECONDS # 7 days
40
31
  DEFAULT_TAGS_TIME_SPAN_SECONDS = 8 * ONE_HOUR_IN_SECONDS # 8 hours
41
32
 
42
33
 
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
-
55
34
  class BaseGrafanaTempoToolset(BaseGrafanaToolset):
56
35
  config_class = GrafanaTempoConfig
57
36
 
@@ -67,6 +46,23 @@ class BaseGrafanaTempoToolset(BaseGrafanaToolset):
67
46
  def grafana_config(self) -> GrafanaTempoConfig:
68
47
  return cast(GrafanaTempoConfig, self._grafana_config)
69
48
 
49
+ def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
50
+ """Check Tempo connectivity using the echo endpoint."""
51
+ # First call parent to validate config
52
+ success, msg = super().prerequisites_callable(config)
53
+ if not success:
54
+ return success, msg
55
+
56
+ # Then check Tempo-specific echo endpoint
57
+ try:
58
+ api = GrafanaTempoAPI(self.grafana_config, use_post=TEMPO_API_USE_POST)
59
+ if api.query_echo_endpoint():
60
+ return True, "Successfully connected to Tempo"
61
+ else:
62
+ return False, "Failed to connect to Tempo echo endpoint"
63
+ except Exception as e:
64
+ return False, f"Failed to connect to Tempo: {str(e)}"
65
+
70
66
  def build_k8s_filters(
71
67
  self, params: Dict[str, Any], use_exact_match: bool
72
68
  ) -> List[str]:
@@ -107,9 +103,9 @@ class BaseGrafanaTempoToolset(BaseGrafanaToolset):
107
103
  escaped_value = value.replace('"', '\\"')
108
104
  filters.append(f'{prefix}{label}="{escaped_value}"')
109
105
  else:
110
- # Escape regex special characters for partial match
111
- escaped_value = re.escape(value)
112
- filters.append(f'{prefix}{label}=~".*{escaped_value}.*"')
106
+ # For partial match, use simple substring matching
107
+ # Don't escape anything - let Tempo handle the regex
108
+ filters.append(f'{prefix}{label}=~".*{value}.*"')
113
109
 
114
110
  return filters
115
111
 
@@ -122,207 +118,198 @@ def validate_params(params: Dict[str, Any], expected_params: List[str]):
122
118
  return f"At least one of the following argument is expected but none were set: {expected_params}"
123
119
 
124
120
 
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,
317
- )
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')})"
121
+ # class GetTempoTraces(Tool):
122
+ # def __init__(self, toolset: BaseGrafanaTempoToolset):
123
+ # super().__init__(
124
+ # name="fetch_tempo_traces",
125
+ # description="""Lists Tempo traces. At least one of `service_name`, `pod_name` or `deployment_name` argument is required.""",
126
+ # parameters={
127
+ # "min_duration": ToolParameter(
128
+ # description="The minimum duration of traces to fetch, e.g., '5s' for 5 seconds.",
129
+ # type="string",
130
+ # required=True,
131
+ # ),
132
+ # "service_name": ToolParameter(
133
+ # description="Filter traces by service name",
134
+ # type="string",
135
+ # required=False,
136
+ # ),
137
+ # "pod_name": ToolParameter(
138
+ # description="Filter traces by pod name",
139
+ # type="string",
140
+ # required=False,
141
+ # ),
142
+ # "namespace_name": ToolParameter(
143
+ # description="Filter traces by namespace",
144
+ # type="string",
145
+ # required=False,
146
+ # ),
147
+ # "deployment_name": ToolParameter(
148
+ # description="Filter traces by deployment name",
149
+ # type="string",
150
+ # required=False,
151
+ # ),
152
+ # "node_name": ToolParameter(
153
+ # description="Filter traces by node",
154
+ # type="string",
155
+ # required=False,
156
+ # ),
157
+ # "start_datetime": ToolParameter(
158
+ # 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}",
159
+ # type="string",
160
+ # required=False,
161
+ # ),
162
+ # "end_datetime": ToolParameter(
163
+ # description="The ending time boundary for the trace search period. String in RFC3339 format. Defaults to NOW().",
164
+ # type="string",
165
+ # required=False,
166
+ # ),
167
+ # "limit": ToolParameter(
168
+ # description="Maximum number of traces to return. Defaults to 50",
169
+ # type="string",
170
+ # required=False,
171
+ # ),
172
+ # "sort": ToolParameter(
173
+ # description="One of 'descending', 'ascending' or 'none' for no sorting. Defaults to descending",
174
+ # type="string",
175
+ # required=False,
176
+ # ),
177
+ # },
178
+ # )
179
+ # self._toolset = toolset
180
+ #
181
+ # def _invoke(self, params: Dict, user_approved: bool = False) -> StructuredToolResult:
182
+ # # Create API instance
183
+ # api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
184
+ #
185
+ # invalid_params_error = validate_params(
186
+ # params, ["service_name", "pod_name", "deployment_name"]
187
+ # )
188
+ # if invalid_params_error:
189
+ # return StructuredToolResult(
190
+ # status=ToolResultStatus.ERROR,
191
+ # error=invalid_params_error,
192
+ # params=params,
193
+ # )
194
+ #
195
+ # start, end = process_timestamps_to_int(
196
+ # params.get("start_datetime"),
197
+ # params.get("end_datetime"),
198
+ # default_time_span_seconds=DEFAULT_TRACES_TIME_SPAN_SECONDS,
199
+ # )
200
+ #
201
+ # filters = self._toolset.build_k8s_filters(params, use_exact_match=True)
202
+ #
203
+ # filters.append(f'duration>{get_param_or_raise(params, "min_duration")}')
204
+ #
205
+ # query = " && ".join(filters)
206
+ # query = f"{{{query}}}"
207
+ #
208
+ # traces = api.search_traces_by_query(
209
+ # q=query,
210
+ # start=start,
211
+ # end=end,
212
+ # limit=params.get("limit", 50),
213
+ # )
214
+ # return StructuredToolResult(
215
+ # status=ToolResultStatus.SUCCESS,
216
+ # data=format_traces_list(traces),
217
+ # params=params,
218
+ # invocation=query,
219
+ # )
220
+ #
221
+ # def get_parameterized_one_liner(self, params: Dict) -> str:
222
+ # return f"{toolset_name_for_one_liner(self._toolset.name)}: Fetched Tempo Traces (min_duration={params.get('min_duration')})"
223
+
224
+
225
+ # class GetTempoTags(Tool):
226
+ # def __init__(self, toolset: BaseGrafanaTempoToolset):
227
+ # super().__init__(
228
+ # name="fetch_tempo_tags",
229
+ # description="List the tags available in Tempo",
230
+ # parameters={
231
+ # "start_datetime": ToolParameter(
232
+ # 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}",
233
+ # type="string",
234
+ # required=False,
235
+ # ),
236
+ # "end_datetime": ToolParameter(
237
+ # description="The ending time boundary for the search period. String in RFC3339 format. Defaults to NOW().",
238
+ # type="string",
239
+ # required=False,
240
+ # ),
241
+ # },
242
+ # )
243
+ # self._toolset = toolset
244
+ #
245
+ # def _invoke(self, params: Dict, user_approved: bool = False) -> StructuredToolResult:
246
+ # # Create API instance
247
+ # api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
248
+ #
249
+ # start, end = process_timestamps_to_int(
250
+ # start=params.get("start_datetime"),
251
+ # end=params.get("end_datetime"),
252
+ # default_time_span_seconds=DEFAULT_TAGS_TIME_SPAN_SECONDS,
253
+ # )
254
+ #
255
+ # try:
256
+ # data = api.search_tag_names_v2(start=start, end=end)
257
+ # return StructuredToolResult(
258
+ # status=ToolResultStatus.SUCCESS,
259
+ # data=yaml.dump(data.get("scopes")),
260
+ # params=params,
261
+ # )
262
+ # except Exception as e:
263
+ # return StructuredToolResult(
264
+ # status=ToolResultStatus.ERROR,
265
+ # error=f"Failed to retrieve tags: {str(e)}",
266
+ # params=params,
267
+ # )
268
+ #
269
+ # def get_parameterized_one_liner(self, params: Dict) -> str:
270
+ # return f"{toolset_name_for_one_liner(self._toolset.name)}: Fetched Tempo tags"
271
+ #
272
+
273
+
274
+ # class GetTempoTraceById(Tool):
275
+ # def __init__(self, toolset: BaseGrafanaTempoToolset):
276
+ # super().__init__(
277
+ # name="fetch_tempo_trace_by_id",
278
+ # description="""Retrieves detailed information about a Tempo trace using its trace ID. Use this to investigate a trace.""",
279
+ # parameters={
280
+ # "trace_id": ToolParameter(
281
+ # description="The unique trace ID to fetch.",
282
+ # type="string",
283
+ # required=True,
284
+ # ),
285
+ # },
286
+ # )
287
+ # self._toolset = toolset
288
+ #
289
+ # def _invoke(self, params: Dict, user_approved: bool = False) -> StructuredToolResult:
290
+ # # Create API instance
291
+ # api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
292
+ #
293
+ # labels_mapping = self._toolset.grafana_config.labels
294
+ # labels = list(labels_mapping.model_dump().values())
295
+ #
296
+ # # Get raw trace data
297
+ # trace_data = api.query_trace_by_id_v2(
298
+ # trace_id=get_param_or_raise(params, "trace_id")
299
+ # )
300
+ #
301
+ # # Process the trace data (new API returns raw data)
302
+ # formatted_trace = process_trace(trace_data, labels)
303
+ #
304
+ # return StructuredToolResult(
305
+ # status=ToolResultStatus.SUCCESS,
306
+ # data=formatted_trace,
307
+ # params=params,
308
+ # )
309
+ #
310
+ # def get_parameterized_one_liner(self, params: Dict) -> str:
311
+ # return f"{toolset_name_for_one_liner(self._toolset.name)}: Fetched Tempo Trace (trace_id={params.get('trace_id')})"
312
+ #
326
313
 
327
314
 
328
315
  class FetchTracesSimpleComparison(Tool):
@@ -426,20 +413,31 @@ Examples:
426
413
  default_time_span_seconds=DEFAULT_TRACES_TIME_SPAN_SECONDS,
427
414
  )
428
415
 
429
- base_url = get_base_url(self._toolset.grafana_config)
416
+ # Create API instance
417
+ api = GrafanaTempoAPI(
418
+ self._toolset.grafana_config, use_post=TEMPO_API_USE_POST
419
+ )
430
420
 
431
421
  # Step 1: Get all trace summaries
432
422
  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,
423
+
424
+ # Debug log the query (useful for troubleshooting)
425
+ import logging
426
+
427
+ logger = logging.getLogger(__name__)
428
+ logger.info(f"Tempo query: {stats_query}")
429
+
430
+ logger.info(f"start: {start}, end: {end}")
431
+
432
+ all_traces_response = api.search_traces_by_query(
433
+ q=stats_query,
438
434
  start=start,
439
435
  end=end,
440
436
  limit=1000,
441
437
  )
442
438
 
439
+ logger.info(f"Response: {all_traces_response}")
440
+
443
441
  traces = all_traces_response.get("traces", [])
444
442
  if not traces:
445
443
  return StructuredToolResult(
@@ -488,39 +486,22 @@ Examples:
488
486
  return None
489
487
 
490
488
  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,
499
- )
500
- response.raise_for_status()
489
+ trace_data = api.query_trace_by_id_v2(trace_id=trace_id)
501
490
  return {
502
491
  "traceID": trace_id,
503
492
  "durationMs": trace_summary.get("durationMs", 0),
504
493
  "rootServiceName": trace_summary.get(
505
494
  "rootServiceName", "unknown"
506
495
  ),
507
- "traceData": response.json(), # Raw trace data
496
+ "traceData": trace_data, # Raw trace data
508
497
  }
509
- except requests.exceptions.RequestException as e:
498
+ except Exception as e:
510
499
  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
500
  return {
514
501
  "traceID": trace_id,
515
502
  "durationMs": trace_summary.get("durationMs", 0),
516
503
  "error": error_msg,
517
504
  }
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
505
 
525
506
  # Fetch the selected traces
526
507
  result = {
@@ -553,6 +534,532 @@ Examples:
553
534
  return f"{toolset_name_for_one_liner(self._toolset.name)}: Simple Tempo Traces Comparison"
554
535
 
555
536
 
537
+ # New tools matching GrafanaTempoAPI methods
538
+
539
+
540
+ class SearchTracesByQuery(Tool):
541
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
542
+ super().__init__(
543
+ name="search_traces_by_query",
544
+ description=(
545
+ "Search for traces using TraceQL query language. "
546
+ "Uses the Tempo API endpoint: GET /api/search with 'q' parameter. "
547
+ 'TraceQL allows complex filtering like: {resource.service.name="api"} && {span.http.status_code=500}'
548
+ ),
549
+ parameters={
550
+ "q": ToolParameter(
551
+ description="TraceQL query (e.g., '{resource.service.name=\"api\" && span.http.status_code=500}')",
552
+ type="string",
553
+ required=True,
554
+ ),
555
+ "limit": ToolParameter(
556
+ description="Maximum number of traces to return",
557
+ type="integer",
558
+ required=False,
559
+ ),
560
+ "start": ToolParameter(
561
+ description="Start time in Unix epoch seconds",
562
+ type="integer",
563
+ required=False,
564
+ ),
565
+ "end": ToolParameter(
566
+ description="End time in Unix epoch seconds",
567
+ type="integer",
568
+ required=False,
569
+ ),
570
+ "spss": ToolParameter(
571
+ description="Spans per span set",
572
+ type="integer",
573
+ required=False,
574
+ ),
575
+ },
576
+ )
577
+ self._toolset = toolset
578
+
579
+ def _invoke(
580
+ self, params: Dict, user_approved: bool = False
581
+ ) -> StructuredToolResult:
582
+ api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
583
+
584
+ try:
585
+ result = api.search_traces_by_query(
586
+ q=params["q"],
587
+ limit=params.get("limit"),
588
+ start=params.get("start"),
589
+ end=params.get("end"),
590
+ spss=params.get("spss"),
591
+ )
592
+ return StructuredToolResult(
593
+ status=ToolResultStatus.SUCCESS,
594
+ data=yaml.dump(result, default_flow_style=False),
595
+ params=params,
596
+ )
597
+ except Exception as e:
598
+ return StructuredToolResult(
599
+ status=ToolResultStatus.ERROR,
600
+ error=str(e),
601
+ params=params,
602
+ )
603
+
604
+ def get_parameterized_one_liner(self, params: Dict) -> str:
605
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Searched traces with TraceQL"
606
+
607
+
608
+ class SearchTracesByTags(Tool):
609
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
610
+ super().__init__(
611
+ name="search_traces_by_tags",
612
+ description=(
613
+ "Search for traces using logfmt-encoded tags. "
614
+ "Uses the Tempo API endpoint: GET /api/search with 'tags' parameter. "
615
+ 'Example: resource.service.name="api" http.status_code="500"'
616
+ ),
617
+ parameters={
618
+ "tags": ToolParameter(
619
+ description='Logfmt-encoded span/process attributes (e.g., \'resource.service.name="api" http.status_code="500"\')',
620
+ type="string",
621
+ required=True,
622
+ ),
623
+ "min_duration": ToolParameter(
624
+ description="Minimum trace duration (e.g., '5s', '100ms')",
625
+ type="string",
626
+ required=False,
627
+ ),
628
+ "max_duration": ToolParameter(
629
+ description="Maximum trace duration (e.g., '10s', '1000ms')",
630
+ type="string",
631
+ required=False,
632
+ ),
633
+ "limit": ToolParameter(
634
+ description="Maximum number of traces to return",
635
+ type="integer",
636
+ required=False,
637
+ ),
638
+ "start": ToolParameter(
639
+ description="Start time in Unix epoch seconds",
640
+ type="integer",
641
+ required=False,
642
+ ),
643
+ "end": ToolParameter(
644
+ description="End time in Unix epoch seconds",
645
+ type="integer",
646
+ required=False,
647
+ ),
648
+ "spss": ToolParameter(
649
+ description="Spans per span set",
650
+ type="integer",
651
+ required=False,
652
+ ),
653
+ },
654
+ )
655
+ self._toolset = toolset
656
+
657
+ def _invoke(
658
+ self, params: Dict, user_approved: bool = False
659
+ ) -> StructuredToolResult:
660
+ api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
661
+
662
+ try:
663
+ result = api.search_traces_by_tags(
664
+ tags=params["tags"],
665
+ min_duration=params.get("min_duration"),
666
+ max_duration=params.get("max_duration"),
667
+ limit=params.get("limit"),
668
+ start=params.get("start"),
669
+ end=params.get("end"),
670
+ spss=params.get("spss"),
671
+ )
672
+ return StructuredToolResult(
673
+ status=ToolResultStatus.SUCCESS,
674
+ data=yaml.dump(result, default_flow_style=False),
675
+ params=params,
676
+ )
677
+ except Exception as e:
678
+ return StructuredToolResult(
679
+ status=ToolResultStatus.ERROR,
680
+ error=str(e),
681
+ params=params,
682
+ )
683
+
684
+ def get_parameterized_one_liner(self, params: Dict) -> str:
685
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Searched traces with tags"
686
+
687
+
688
+ class QueryTraceById(Tool):
689
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
690
+ super().__init__(
691
+ name="query_trace_by_id",
692
+ description=(
693
+ "Retrieve detailed trace information by trace ID. "
694
+ "Uses the Tempo API endpoint: GET /api/v2/traces/{trace_id}. "
695
+ "Returns the full trace data in OpenTelemetry format."
696
+ ),
697
+ parameters={
698
+ "trace_id": ToolParameter(
699
+ description="The unique trace ID to fetch",
700
+ type="string",
701
+ required=True,
702
+ ),
703
+ "start": ToolParameter(
704
+ description="Optional start time in Unix epoch seconds",
705
+ type="integer",
706
+ required=False,
707
+ ),
708
+ "end": ToolParameter(
709
+ description="Optional end time in Unix epoch seconds",
710
+ type="integer",
711
+ required=False,
712
+ ),
713
+ },
714
+ )
715
+ self._toolset = toolset
716
+
717
+ def _invoke(
718
+ self, params: Dict, user_approved: bool = False
719
+ ) -> StructuredToolResult:
720
+ api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
721
+
722
+ try:
723
+ trace_data = api.query_trace_by_id_v2(
724
+ trace_id=params["trace_id"],
725
+ start=params.get("start"),
726
+ end=params.get("end"),
727
+ )
728
+
729
+ # Return raw trace data as YAML for readability
730
+ return StructuredToolResult(
731
+ status=ToolResultStatus.SUCCESS,
732
+ data=yaml.dump(trace_data, default_flow_style=False),
733
+ params=params,
734
+ )
735
+ except Exception as e:
736
+ return StructuredToolResult(
737
+ status=ToolResultStatus.ERROR,
738
+ error=str(e),
739
+ params=params,
740
+ )
741
+
742
+ def get_parameterized_one_liner(self, params: Dict) -> str:
743
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Retrieved trace {params.get('trace_id')}"
744
+
745
+
746
+ class SearchTagNames(Tool):
747
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
748
+ super().__init__(
749
+ name="search_tag_names",
750
+ description=(
751
+ "Discover available tag names across traces. "
752
+ "Uses the Tempo API endpoint: GET /api/v2/search/tags. "
753
+ "Returns tags organized by scope (resource, span, intrinsic)."
754
+ ),
755
+ parameters={
756
+ "scope": ToolParameter(
757
+ description="Filter by scope: 'resource', 'span', or 'intrinsic'",
758
+ type="string",
759
+ required=False,
760
+ ),
761
+ "q": ToolParameter(
762
+ description="TraceQL query to filter tags (e.g., '{resource.cluster=\"us-east-1\"}')",
763
+ type="string",
764
+ required=False,
765
+ ),
766
+ "start": ToolParameter(
767
+ description="Start time in Unix epoch seconds",
768
+ type="integer",
769
+ required=False,
770
+ ),
771
+ "end": ToolParameter(
772
+ description="End time in Unix epoch seconds",
773
+ type="integer",
774
+ required=False,
775
+ ),
776
+ "limit": ToolParameter(
777
+ description="Maximum number of tag names to return",
778
+ type="integer",
779
+ required=False,
780
+ ),
781
+ "max_stale_values": ToolParameter(
782
+ description="Maximum stale values parameter",
783
+ type="integer",
784
+ required=False,
785
+ ),
786
+ },
787
+ )
788
+ self._toolset = toolset
789
+
790
+ def _invoke(
791
+ self, params: Dict, user_approved: bool = False
792
+ ) -> StructuredToolResult:
793
+ api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
794
+
795
+ try:
796
+ result = api.search_tag_names_v2(
797
+ scope=params.get("scope"),
798
+ q=params.get("q"),
799
+ start=params.get("start"),
800
+ end=params.get("end"),
801
+ limit=params.get("limit"),
802
+ max_stale_values=params.get("max_stale_values"),
803
+ )
804
+ return StructuredToolResult(
805
+ status=ToolResultStatus.SUCCESS,
806
+ data=yaml.dump(result, default_flow_style=False),
807
+ params=params,
808
+ )
809
+ except Exception as e:
810
+ return StructuredToolResult(
811
+ status=ToolResultStatus.ERROR,
812
+ error=str(e),
813
+ params=params,
814
+ )
815
+
816
+ def get_parameterized_one_liner(self, params: Dict) -> str:
817
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Discovered tag names"
818
+
819
+
820
+ class SearchTagValues(Tool):
821
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
822
+ super().__init__(
823
+ name="search_tag_values",
824
+ description=(
825
+ "Get all values for a specific tag. "
826
+ "Uses the Tempo API endpoint: GET /api/v2/search/tag/{tag}/values. "
827
+ "Useful for discovering what values exist for a given tag."
828
+ ),
829
+ parameters={
830
+ "tag": ToolParameter(
831
+ description="The tag name to get values for (e.g., 'resource.service.name', 'http.status_code')",
832
+ type="string",
833
+ required=True,
834
+ ),
835
+ "q": ToolParameter(
836
+ description="TraceQL query to filter tag values (e.g., '{resource.cluster=\"us-east-1\"}')",
837
+ type="string",
838
+ required=False,
839
+ ),
840
+ "start": ToolParameter(
841
+ description="Start time in Unix epoch seconds",
842
+ type="integer",
843
+ required=False,
844
+ ),
845
+ "end": ToolParameter(
846
+ description="End time in Unix epoch seconds",
847
+ type="integer",
848
+ required=False,
849
+ ),
850
+ "limit": ToolParameter(
851
+ description="Maximum number of values to return",
852
+ type="integer",
853
+ required=False,
854
+ ),
855
+ "max_stale_values": ToolParameter(
856
+ description="Maximum stale values parameter",
857
+ type="integer",
858
+ required=False,
859
+ ),
860
+ },
861
+ )
862
+ self._toolset = toolset
863
+
864
+ def _invoke(
865
+ self, params: Dict, user_approved: bool = False
866
+ ) -> StructuredToolResult:
867
+ api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
868
+
869
+ try:
870
+ result = api.search_tag_values_v2(
871
+ tag=params["tag"],
872
+ q=params.get("q"),
873
+ start=params.get("start"),
874
+ end=params.get("end"),
875
+ limit=params.get("limit"),
876
+ max_stale_values=params.get("max_stale_values"),
877
+ )
878
+ return StructuredToolResult(
879
+ status=ToolResultStatus.SUCCESS,
880
+ data=yaml.dump(result, default_flow_style=False),
881
+ params=params,
882
+ )
883
+ except Exception as e:
884
+ return StructuredToolResult(
885
+ status=ToolResultStatus.ERROR,
886
+ error=str(e),
887
+ params=params,
888
+ )
889
+
890
+ def get_parameterized_one_liner(self, params: Dict) -> str:
891
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Retrieved values for tag '{params.get('tag')}'"
892
+
893
+
894
+ class QueryMetricsInstant(Tool):
895
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
896
+ super().__init__(
897
+ name="query_metrics_instant",
898
+ description=(
899
+ "Compute a single TraceQL metric value across time range. "
900
+ "Uses the Tempo API endpoint: GET /api/metrics/query. "
901
+ "TraceQL metrics compute aggregated metrics from trace data. "
902
+ "Returns a single value for the entire time range. "
903
+ "Basic syntax: {selector} | function(attribute) [by (grouping)]\n\n"
904
+ "TraceQL metrics can help answer questions like:\n"
905
+ "- How many database calls across all systems are downstream of your application?\n"
906
+ "- What services beneath a given endpoint are failing?\n"
907
+ "- What services beneath an endpoint are slow?\n\n"
908
+ "TraceQL metrics help you answer these questions by parsing your traces in aggregate. "
909
+ "The instant version returns a single value for the query and is preferred over "
910
+ "query_metrics_range when you don't need the granularity of a full time-series but want "
911
+ "a total sum or single value computed across the whole time range."
912
+ ),
913
+ parameters={
914
+ "q": ToolParameter(
915
+ description=(
916
+ "TraceQL metrics query. Supported functions: rate, count_over_time, "
917
+ "sum_over_time, max_over_time, min_over_time, avg_over_time, "
918
+ "quantile_over_time, histogram_over_time, compare. "
919
+ "Can use topk or bottomk modifiers. "
920
+ "Syntax: {selector} | function(attribute) [by (grouping)]. "
921
+ 'Example: {resource.service.name="api"} | avg_over_time(duration)'
922
+ ),
923
+ type="string",
924
+ required=True,
925
+ ),
926
+ "start": ToolParameter(
927
+ description="Start time (Unix seconds/nanoseconds/RFC3339)",
928
+ type="string",
929
+ required=False,
930
+ ),
931
+ "end": ToolParameter(
932
+ description="End time (Unix seconds/nanoseconds/RFC3339)",
933
+ type="string",
934
+ required=False,
935
+ ),
936
+ "since": ToolParameter(
937
+ description="Duration string (e.g., '1h', '30m')",
938
+ type="string",
939
+ required=False,
940
+ ),
941
+ },
942
+ )
943
+ self._toolset = toolset
944
+
945
+ def _invoke(
946
+ self, params: Dict, user_approved: bool = False
947
+ ) -> StructuredToolResult:
948
+ api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
949
+
950
+ try:
951
+ result = api.query_metrics_instant(
952
+ q=params["q"],
953
+ start=params.get("start"),
954
+ end=params.get("end"),
955
+ since=params.get("since"),
956
+ )
957
+ return StructuredToolResult(
958
+ status=ToolResultStatus.SUCCESS,
959
+ data=yaml.dump(result, default_flow_style=False),
960
+ params=params,
961
+ )
962
+ except Exception as e:
963
+ return StructuredToolResult(
964
+ status=ToolResultStatus.ERROR,
965
+ error=str(e),
966
+ params=params,
967
+ )
968
+
969
+ def get_parameterized_one_liner(self, params: Dict) -> str:
970
+ return (
971
+ f"{toolset_name_for_one_liner(self._toolset.name)}: Computed TraceQL metric"
972
+ )
973
+
974
+
975
+ class QueryMetricsRange(Tool):
976
+ def __init__(self, toolset: BaseGrafanaTempoToolset):
977
+ super().__init__(
978
+ name="query_metrics_range",
979
+ description=(
980
+ "Get time series data from TraceQL metrics queries. "
981
+ "Uses the Tempo API endpoint: GET /api/metrics/query_range. "
982
+ "Returns metrics computed at regular intervals (controlled by 'step' parameter). "
983
+ "Use this for graphing metrics over time or analyzing trends. "
984
+ "Basic syntax: {selector} | function(attribute) [by (grouping)]\n\n"
985
+ "TraceQL metrics can help answer questions like:\n"
986
+ "- How many database calls across all systems are downstream of your application?\n"
987
+ "- What services beneath a given endpoint are failing?\n"
988
+ "- What services beneath an endpoint are slow?\n\n"
989
+ "TraceQL metrics help you answer these questions by parsing your traces in aggregate."
990
+ ),
991
+ parameters={
992
+ "q": ToolParameter(
993
+ description=(
994
+ "TraceQL metrics query. Supported functions: rate, count_over_time, "
995
+ "sum_over_time, max_over_time, min_over_time, avg_over_time, "
996
+ "quantile_over_time, histogram_over_time, compare. "
997
+ "Can use topk or bottomk modifiers. "
998
+ "Syntax: {selector} | function(attribute) [by (grouping)]. "
999
+ 'Example: {resource.service.name="api"} | avg_over_time(duration)'
1000
+ ),
1001
+ type="string",
1002
+ required=True,
1003
+ ),
1004
+ "step": ToolParameter(
1005
+ description="Time series granularity (e.g., '1m', '5m', '1h')",
1006
+ type="string",
1007
+ required=False,
1008
+ ),
1009
+ "start": ToolParameter(
1010
+ description="Start time (Unix seconds/nanoseconds/RFC3339)",
1011
+ type="string",
1012
+ required=False,
1013
+ ),
1014
+ "end": ToolParameter(
1015
+ description="End time (Unix seconds/nanoseconds/RFC3339)",
1016
+ type="string",
1017
+ required=False,
1018
+ ),
1019
+ "since": ToolParameter(
1020
+ description="Duration string (e.g., '3h', '1d')",
1021
+ type="string",
1022
+ required=False,
1023
+ ),
1024
+ "exemplars": ToolParameter(
1025
+ description="Maximum number of exemplars to return",
1026
+ type="integer",
1027
+ required=False,
1028
+ ),
1029
+ },
1030
+ )
1031
+ self._toolset = toolset
1032
+
1033
+ def _invoke(
1034
+ self, params: Dict, user_approved: bool = False
1035
+ ) -> StructuredToolResult:
1036
+ api = GrafanaTempoAPI(self._toolset.grafana_config, use_post=TEMPO_API_USE_POST)
1037
+
1038
+ try:
1039
+ result = api.query_metrics_range(
1040
+ q=params["q"],
1041
+ step=params.get("step"),
1042
+ start=params.get("start"),
1043
+ end=params.get("end"),
1044
+ since=params.get("since"),
1045
+ exemplars=params.get("exemplars"),
1046
+ )
1047
+ return StructuredToolResult(
1048
+ status=ToolResultStatus.SUCCESS,
1049
+ data=yaml.dump(result, default_flow_style=False),
1050
+ params=params,
1051
+ )
1052
+ except Exception as e:
1053
+ return StructuredToolResult(
1054
+ status=ToolResultStatus.ERROR,
1055
+ error=str(e),
1056
+ params=params,
1057
+ )
1058
+
1059
+ def get_parameterized_one_liner(self, params: Dict) -> str:
1060
+ return f"{toolset_name_for_one_liner(self._toolset.name)}: Retrieved TraceQL metrics time series"
1061
+
1062
+
556
1063
  class GrafanaTempoToolset(BaseGrafanaTempoToolset):
557
1064
  def __init__(self):
558
1065
  super().__init__(
@@ -562,9 +1069,16 @@ class GrafanaTempoToolset(BaseGrafanaTempoToolset):
562
1069
  docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/grafanatempo/",
563
1070
  tools=[
564
1071
  FetchTracesSimpleComparison(self),
565
- GetTempoTraces(self),
566
- GetTempoTraceById(self),
567
- GetTempoTags(self),
1072
+ # GetTempoTraces(self),
1073
+ # GetTempoTraceById(self),
1074
+ # GetTempoTags(self),
1075
+ SearchTracesByQuery(self),
1076
+ SearchTracesByTags(self),
1077
+ QueryTraceById(self),
1078
+ SearchTagNames(self),
1079
+ SearchTagValues(self),
1080
+ QueryMetricsInstant(self),
1081
+ QueryMetricsRange(self),
568
1082
  ],
569
1083
  )
570
1084
  template_file_path = os.path.abspath(