holmesgpt 0.13.3a0__py3-none-any.whl → 0.14.1__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 (86) hide show
  1. holmes/__init__.py +1 -1
  2. holmes/clients/robusta_client.py +15 -4
  3. holmes/common/env_vars.py +8 -1
  4. holmes/config.py +66 -139
  5. holmes/core/investigation.py +1 -2
  6. holmes/core/llm.py +295 -52
  7. holmes/core/models.py +2 -0
  8. holmes/core/safeguards.py +4 -4
  9. holmes/core/supabase_dal.py +14 -8
  10. holmes/core/tool_calling_llm.py +202 -177
  11. holmes/core/tools.py +260 -25
  12. holmes/core/tools_utils/data_types.py +81 -0
  13. holmes/core/tools_utils/tool_context_window_limiter.py +33 -0
  14. holmes/core/tools_utils/tool_executor.py +2 -2
  15. holmes/core/toolset_manager.py +150 -3
  16. holmes/core/tracing.py +6 -1
  17. holmes/core/transformers/__init__.py +23 -0
  18. holmes/core/transformers/base.py +62 -0
  19. holmes/core/transformers/llm_summarize.py +174 -0
  20. holmes/core/transformers/registry.py +122 -0
  21. holmes/core/transformers/transformer.py +31 -0
  22. holmes/main.py +5 -0
  23. holmes/plugins/prompts/_fetch_logs.jinja2 +10 -1
  24. holmes/plugins/toolsets/aks-node-health.yaml +46 -0
  25. holmes/plugins/toolsets/aks.yaml +64 -0
  26. holmes/plugins/toolsets/atlas_mongodb/mongodb_atlas.py +17 -15
  27. holmes/plugins/toolsets/azure_sql/tools/analyze_connection_failures.py +8 -4
  28. holmes/plugins/toolsets/azure_sql/tools/analyze_database_connections.py +7 -3
  29. holmes/plugins/toolsets/azure_sql/tools/analyze_database_health_status.py +3 -3
  30. holmes/plugins/toolsets/azure_sql/tools/analyze_database_performance.py +3 -3
  31. holmes/plugins/toolsets/azure_sql/tools/analyze_database_storage.py +7 -3
  32. holmes/plugins/toolsets/azure_sql/tools/get_active_alerts.py +4 -4
  33. holmes/plugins/toolsets/azure_sql/tools/get_slow_queries.py +7 -3
  34. holmes/plugins/toolsets/azure_sql/tools/get_top_cpu_queries.py +7 -3
  35. holmes/plugins/toolsets/azure_sql/tools/get_top_data_io_queries.py +7 -3
  36. holmes/plugins/toolsets/azure_sql/tools/get_top_log_io_queries.py +7 -3
  37. holmes/plugins/toolsets/bash/bash_toolset.py +6 -6
  38. holmes/plugins/toolsets/bash/common/bash.py +7 -7
  39. holmes/plugins/toolsets/coralogix/toolset_coralogix_logs.py +5 -3
  40. holmes/plugins/toolsets/datadog/datadog_api.py +490 -24
  41. holmes/plugins/toolsets/datadog/datadog_logs_instructions.jinja2 +21 -10
  42. holmes/plugins/toolsets/datadog/toolset_datadog_general.py +345 -207
  43. holmes/plugins/toolsets/datadog/toolset_datadog_logs.py +190 -19
  44. holmes/plugins/toolsets/datadog/toolset_datadog_metrics.py +96 -32
  45. holmes/plugins/toolsets/datadog/toolset_datadog_rds.py +10 -10
  46. holmes/plugins/toolsets/datadog/toolset_datadog_traces.py +21 -22
  47. holmes/plugins/toolsets/git.py +22 -22
  48. holmes/plugins/toolsets/grafana/common.py +14 -2
  49. holmes/plugins/toolsets/grafana/grafana_tempo_api.py +473 -0
  50. holmes/plugins/toolsets/grafana/toolset_grafana.py +4 -4
  51. holmes/plugins/toolsets/grafana/toolset_grafana_loki.py +5 -4
  52. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.jinja2 +246 -11
  53. holmes/plugins/toolsets/grafana/toolset_grafana_tempo.py +662 -290
  54. holmes/plugins/toolsets/grafana/trace_parser.py +1 -1
  55. holmes/plugins/toolsets/internet/internet.py +3 -3
  56. holmes/plugins/toolsets/internet/notion.py +3 -3
  57. holmes/plugins/toolsets/investigator/core_investigation.py +3 -3
  58. holmes/plugins/toolsets/kafka.py +18 -18
  59. holmes/plugins/toolsets/kubernetes.yaml +58 -0
  60. holmes/plugins/toolsets/kubernetes_logs.py +6 -6
  61. holmes/plugins/toolsets/kubernetes_logs.yaml +32 -0
  62. holmes/plugins/toolsets/logging_utils/logging_api.py +1 -1
  63. holmes/plugins/toolsets/mcp/toolset_mcp.py +4 -4
  64. holmes/plugins/toolsets/newrelic.py +8 -8
  65. holmes/plugins/toolsets/opensearch/opensearch.py +5 -5
  66. holmes/plugins/toolsets/opensearch/opensearch_logs.py +7 -7
  67. holmes/plugins/toolsets/opensearch/opensearch_traces.py +10 -10
  68. holmes/plugins/toolsets/prometheus/prometheus.py +841 -351
  69. holmes/plugins/toolsets/prometheus/prometheus_instructions.jinja2 +39 -2
  70. holmes/plugins/toolsets/prometheus/utils.py +28 -0
  71. holmes/plugins/toolsets/rabbitmq/toolset_rabbitmq.py +6 -4
  72. holmes/plugins/toolsets/robusta/robusta.py +10 -10
  73. holmes/plugins/toolsets/runbook/runbook_fetcher.py +4 -4
  74. holmes/plugins/toolsets/servicenow/servicenow.py +6 -6
  75. holmes/plugins/toolsets/utils.py +88 -0
  76. holmes/utils/config_utils.py +91 -0
  77. holmes/utils/env.py +7 -0
  78. holmes/utils/holmes_status.py +2 -1
  79. holmes/utils/sentry_helper.py +41 -0
  80. holmes/utils/stream.py +9 -0
  81. {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1.dist-info}/METADATA +11 -15
  82. {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1.dist-info}/RECORD +85 -75
  83. holmes/plugins/toolsets/grafana/tempo_api.py +0 -124
  84. {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1.dist-info}/LICENSE.txt +0 -0
  85. {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1.dist-info}/WHEEL +0 -0
  86. {holmesgpt-0.13.3a0.dist-info → holmesgpt-0.14.1.dist-info}/entry_points.txt +0 -0
@@ -13,7 +13,7 @@ from holmes.core.tools import (
13
13
  ToolParameter,
14
14
  Toolset,
15
15
  StructuredToolResult,
16
- ToolResultStatus,
16
+ StructuredToolResultStatus,
17
17
  ToolsetTag,
18
18
  )
19
19
  from holmes.plugins.toolsets.consts import TOOLSET_CONFIG_MISSING_ERROR
@@ -23,85 +23,120 @@ from holmes.plugins.toolsets.datadog.datadog_api import (
23
23
  execute_datadog_http_request,
24
24
  get_headers,
25
25
  MAX_RETRY_COUNT_ON_RATE_LIMIT,
26
+ preprocess_time_fields,
27
+ enhance_error_message,
28
+ fetch_openapi_spec,
26
29
  )
27
30
  from holmes.plugins.toolsets.utils import toolset_name_for_one_liner
28
31
 
29
32
  # Maximum response size in bytes (10MB)
30
33
  MAX_RESPONSE_SIZE = 10 * 1024 * 1024
31
34
 
32
- # Whitelisted API endpoint patterns - READ ONLY operations
35
+ # Whitelisted API endpoint patterns with optional hints
36
+ # Format: (pattern, hint) - hint is empty string if no special instructions
33
37
  WHITELISTED_ENDPOINTS = [
34
38
  # Monitors
35
- r"^/api/v\d+/monitor(/search)?$",
36
- r"^/api/v\d+/monitor/\d+(/downtimes)?$",
37
- r"^/api/v\d+/monitor/groups/search$",
39
+ (r"^/api/v\d+/monitor(/search)?$", ""),
40
+ (r"^/api/v\d+/monitor/\d+(/downtimes)?$", ""),
41
+ (r"^/api/v\d+/monitor/groups/search$", ""),
38
42
  # Dashboards
39
- r"^/api/v\d+/dashboard(/lists)?$",
40
- r"^/api/v\d+/dashboard/[^/]+$",
41
- r"^/api/v\d+/dashboard/public/[^/]+$",
42
- # SLOs (Service Level Objectives)
43
- r"^/api/v\d+/slo(/search)?$",
44
- r"^/api/v\d+/slo/[^/]+(/history)?$",
45
- r"^/api/v\d+/slo/[^/]+/corrections$",
43
+ (r"^/api/v\d+/dashboard(/lists)?$", ""),
44
+ (r"^/api/v\d+/dashboard/[^/]+$", ""),
45
+ (r"^/api/v\d+/dashboard/public/[^/]+$", ""),
46
+ # SLOs
47
+ (r"^/api/v\d+/slo(/search)?$", ""),
48
+ (r"^/api/v\d+/slo/[^/]+(/history)?$", ""),
49
+ (r"^/api/v\d+/slo/[^/]+/corrections$", ""),
46
50
  # Events
47
- r"^/api/v\d+/events$",
48
- r"^/api/v\d+/events/\d+$",
51
+ (
52
+ r"^/api/v\d+/events$",
53
+ "Use time range parameters 'start' and 'end' as Unix timestamps",
54
+ ),
55
+ (r"^/api/v\d+/events/\d+$", ""),
49
56
  # Incidents
50
- r"^/api/v\d+/incidents(/search)?$",
51
- r"^/api/v\d+/incidents/[^/]+$",
52
- r"^/api/v\d+/incidents/[^/]+/attachments$",
53
- r"^/api/v\d+/incidents/[^/]+/connected_integrations$",
54
- r"^/api/v\d+/incidents/[^/]+/relationships$",
55
- r"^/api/v\d+/incidents/[^/]+/timeline$",
57
+ (r"^/api/v\d+/incidents(/search)?$", ""),
58
+ (r"^/api/v\d+/incidents/[^/]+$", ""),
59
+ (r"^/api/v\d+/incidents/[^/]+/attachments$", ""),
60
+ (r"^/api/v\d+/incidents/[^/]+/connected_integrations$", ""),
61
+ (r"^/api/v\d+/incidents/[^/]+/relationships$", ""),
62
+ (r"^/api/v\d+/incidents/[^/]+/timeline$", ""),
56
63
  # Synthetics
57
- r"^/api/v\d+/synthetics/tests(/search)?$",
58
- r"^/api/v\d+/synthetics/tests/[^/]+$",
59
- r"^/api/v\d+/synthetics/tests/[^/]+/results$",
60
- r"^/api/v\d+/synthetics/tests/browser/[^/]+/results$",
61
- r"^/api/v\d+/synthetics/tests/api/[^/]+/results$",
62
- r"^/api/v\d+/synthetics/locations$",
63
- # Security Monitoring
64
- r"^/api/v\d+/security_monitoring/rules(/search)?$",
65
- r"^/api/v\d+/security_monitoring/rules/[^/]+$",
66
- r"^/api/v\d+/security_monitoring/signals(/search)?$",
67
- r"^/api/v\d+/security_monitoring/signals/[^/]+$",
68
- # Service Map / APM Services
69
- r"^/api/v\d+/services$",
70
- r"^/api/v\d+/services/[^/]+$",
71
- r"^/api/v\d+/services/[^/]+/dependencies$",
64
+ (r"^/api/v\d+/synthetics/tests(/search)?$", ""),
65
+ (r"^/api/v\d+/synthetics/tests/[^/]+$", ""),
66
+ (r"^/api/v\d+/synthetics/tests/[^/]+/results$", ""),
67
+ (r"^/api/v\d+/synthetics/tests/browser/[^/]+/results$", ""),
68
+ (r"^/api/v\d+/synthetics/tests/api/[^/]+/results$", ""),
69
+ (r"^/api/v\d+/synthetics/locations$", ""),
70
+ # Security
71
+ (r"^/api/v\d+/security_monitoring/rules(/search)?$", ""),
72
+ (r"^/api/v\d+/security_monitoring/rules/[^/]+$", ""),
73
+ (r"^/api/v\d+/security_monitoring/signals(/search)?$", ""),
74
+ (r"^/api/v\d+/security_monitoring/signals/[^/]+$", ""),
75
+ # Services
76
+ (r"^/api/v\d+/services$", ""),
77
+ (r"^/api/v\d+/services/[^/]+$", ""),
78
+ (r"^/api/v\d+/services/[^/]+/dependencies$", ""),
79
+ (r"^/api/v\d+/service_dependencies$", ""),
72
80
  # Hosts
73
- r"^/api/v\d+/hosts$",
74
- r"^/api/v\d+/hosts/totals$",
75
- r"^/api/v\d+/hosts/[^/]+$",
76
- # Usage & Cost
77
- r"^/api/v\d+/usage/[^/]+$",
78
- r"^/api/v\d+/usage/summary$",
79
- r"^/api/v\d+/usage/billable-summary$",
80
- r"^/api/v\d+/usage/cost_by_org$",
81
- r"^/api/v\d+/usage/estimated_cost$",
81
+ (r"^/api/v\d+/hosts$", ""),
82
+ (r"^/api/v\d+/hosts/totals$", ""),
83
+ (r"^/api/v\d+/hosts/[^/]+$", ""),
84
+ # Usage
85
+ (r"^/api/v\d+/usage/[^/]+$", ""),
86
+ (r"^/api/v\d+/usage/summary$", ""),
87
+ (r"^/api/v\d+/usage/billable-summary$", ""),
88
+ (r"^/api/v\d+/usage/cost_by_org$", ""),
89
+ (r"^/api/v\d+/usage/estimated_cost$", ""),
82
90
  # Processes
83
- r"^/api/v\d+/processes$",
91
+ (r"^/api/v\d+/processes$", ""),
84
92
  # Tags
85
- r"^/api/v\d+/tags/hosts(/[^/]+)?$",
93
+ (r"^/api/v\d+/tags/hosts(/[^/]+)?$", ""),
86
94
  # Notebooks
87
- r"^/api/v\d+/notebooks$",
88
- r"^/api/v\d+/notebooks/\d+$",
89
- # Service Dependencies
90
- r"^/api/v\d+/service_dependencies$",
95
+ (r"^/api/v\d+/notebooks$", ""),
96
+ (r"^/api/v\d+/notebooks/\d+$", ""),
91
97
  # Organization
92
- r"^/api/v\d+/org$",
93
- r"^/api/v\d+/org/[^/]+$",
94
- # Users (read only)
95
- r"^/api/v\d+/users$",
96
- r"^/api/v\d+/users/[^/]+$",
97
- # Teams (read only)
98
- r"^/api/v\d+/teams$",
99
- r"^/api/v\d+/teams/[^/]+$",
100
- # Audit logs
101
- r"^/api/v\d+/audit/events$",
102
- # Service Accounts (read only)
103
- r"^/api/v\d+/service_accounts$",
104
- r"^/api/v\d+/service_accounts/[^/]+$",
98
+ (r"^/api/v\d+/org$", ""),
99
+ (r"^/api/v\d+/org/[^/]+$", ""),
100
+ # Users
101
+ (r"^/api/v\d+/users$", ""),
102
+ (r"^/api/v\d+/users/[^/]+$", ""),
103
+ # Teams
104
+ (r"^/api/v\d+/teams$", ""),
105
+ (r"^/api/v\d+/teams/[^/]+$", ""),
106
+ # Logs
107
+ (
108
+ r"^/api/v1/logs/config/indexes$",
109
+ "When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset",
110
+ ),
111
+ (
112
+ r"^/api/v2/logs/events$",
113
+ "When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset. Use RFC3339 timestamps (e.g., '2024-01-01T00:00:00Z')",
114
+ ),
115
+ (
116
+ r"^/api/v2/logs/events/search$",
117
+ 'When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset. RFC3339 time format. Example: {"filter": {"from": "2024-01-01T00:00:00Z", "to": "2024-01-02T00:00:00Z", "query": "*"}}',
118
+ ),
119
+ (
120
+ r"^/api/v2/logs/analytics/aggregate$",
121
+ "When available, prefer using fetch_pod_logs tool from datadog/logs toolset instead of calling this API directly with the datadog/general toolset. Do not include 'sort' parameter",
122
+ ),
123
+ # Metrics
124
+ (
125
+ r"^/api/v\d+/metrics$",
126
+ "When available, prefer using query_datadog_metrics tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset",
127
+ ),
128
+ (
129
+ r"^/api/v\d+/metrics/[^/]+$",
130
+ "When available, prefer using get_datadog_metric_metadata tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset",
131
+ ),
132
+ (
133
+ r"^/api/v\d+/query$",
134
+ "When available, prefer using query_datadog_metrics tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset. Use 'from' and 'to' as Unix timestamps",
135
+ ),
136
+ (
137
+ r"^/api/v\d+/search/query$",
138
+ "When available, prefer using query_datadog_metrics tool from datadog/metrics toolset instead of calling this API directly with the datadog/general toolset",
139
+ ),
105
140
  ]
106
141
 
107
142
  # Blacklisted path segments that indicate write operations
@@ -146,9 +181,13 @@ WHITELISTED_POST_ENDPOINTS = [
146
181
  r"^/api/v\d+/security_monitoring/rules/search$",
147
182
  r"^/api/v\d+/security_monitoring/signals/search$",
148
183
  r"^/api/v\d+/logs/events/search$",
184
+ r"^/api/v2/logs/events/search$",
185
+ r"^/api/v2/logs/analytics/aggregate$",
149
186
  r"^/api/v\d+/spans/events/search$",
150
187
  r"^/api/v\d+/rum/events/search$",
151
188
  r"^/api/v\d+/audit/events/search$",
189
+ r"^/api/v\d+/query$",
190
+ r"^/api/v\d+/search/query$",
152
191
  ]
153
192
 
154
193
 
@@ -165,12 +204,13 @@ class DatadogGeneralToolset(Toolset):
165
204
  """General-purpose Datadog API toolset for read-only operations not covered by specialized toolsets."""
166
205
 
167
206
  dd_config: Optional[DatadogGeneralConfig] = None
207
+ openapi_spec: Optional[Dict[str, Any]] = None
168
208
 
169
209
  def __init__(self):
170
210
  super().__init__(
171
211
  name="datadog/general",
172
- description="General-purpose Datadog API access for read-only operations including monitors, dashboards, SLOs, incidents, synthetics, and more",
173
- docs_url="https://docs.datadoghq.com/api/latest/",
212
+ description="General-purpose Datadog API access for read-only operations including monitors, dashboards, SLOs, incidents, synthetics, logs, metrics, and more. Note: For logs and metrics, prefer using the specialized datadog/logs and datadog/metrics toolsets when available as they provide optimized functionality",
213
+ docs_url="https://holmesgpt.dev/data-sources/builtin-toolsets/datadog/",
174
214
  icon_url="https://imgix.datadoghq.com//img/about/presskit/DDlogo.jpg",
175
215
  prerequisites=[CallablePrerequisite(callable=self.prerequisites_callable)],
176
216
  tools=[
@@ -178,7 +218,6 @@ class DatadogGeneralToolset(Toolset):
178
218
  DatadogAPIPostSearch(toolset=self),
179
219
  ListDatadogAPIResources(toolset=self),
180
220
  ],
181
- experimental=True,
182
221
  tags=[ToolsetTag.CORE],
183
222
  )
184
223
  template_file_path = os.path.abspath(
@@ -191,11 +230,27 @@ class DatadogGeneralToolset(Toolset):
191
230
  def prerequisites_callable(self, config: dict[str, Any]) -> Tuple[bool, str]:
192
231
  """Check prerequisites with configuration."""
193
232
  if not config:
194
- return False, TOOLSET_CONFIG_MISSING_ERROR
233
+ return (
234
+ False,
235
+ "Datadog general toolset requires configuration. Please provide: dd_api_key, dd_app_key, and site_api_url in your Holmes config. For more details, see https://holmesgpt.dev/data-sources/builtin-toolsets/datadog/",
236
+ )
195
237
 
196
238
  try:
197
239
  dd_config = DatadogGeneralConfig(**config)
198
240
  self.dd_config = dd_config
241
+
242
+ # Fetch OpenAPI spec on startup for better error messages and documentation
243
+ logging.debug("Fetching Datadog OpenAPI specification...")
244
+ self.openapi_spec = fetch_openapi_spec(version="both")
245
+ if self.openapi_spec:
246
+ logging.info(
247
+ f"Successfully loaded OpenAPI spec with {len(self.openapi_spec.get('paths', {}))} endpoints"
248
+ )
249
+ else:
250
+ logging.warning(
251
+ "Could not fetch OpenAPI spec; enhanced error messages will be limited"
252
+ )
253
+
199
254
  success, error_msg = self._perform_healthcheck(dd_config)
200
255
  return success, error_msg
201
256
  except Exception as e:
@@ -206,7 +261,8 @@ class DatadogGeneralToolset(Toolset):
206
261
  """Perform health check on Datadog API."""
207
262
  try:
208
263
  logging.info("Performing Datadog general API configuration healthcheck...")
209
- url = f"{dd_config.site_api_url}/api/v1/validate"
264
+ base_url = str(dd_config.site_api_url).rstrip("/")
265
+ url = f"{base_url}/api/v1/validate"
210
266
  headers = get_headers(dd_config)
211
267
 
212
268
  data = execute_datadog_http_request(
@@ -218,7 +274,7 @@ class DatadogGeneralToolset(Toolset):
218
274
  )
219
275
 
220
276
  if data.get("valid", False):
221
- logging.info("Datadog general API healthcheck completed successfully")
277
+ logging.debug("Datadog general API healthcheck completed successfully")
222
278
  return True, ""
223
279
  else:
224
280
  error_msg = "Datadog API key validation failed"
@@ -267,7 +323,7 @@ def is_endpoint_allowed(
267
323
  return False, f"POST method not allowed for endpoint: {path}"
268
324
 
269
325
  elif method == "GET":
270
- for pattern in WHITELISTED_ENDPOINTS:
326
+ for pattern, _ in WHITELISTED_ENDPOINTS:
271
327
  if re.match(pattern, path):
272
328
  return True, ""
273
329
 
@@ -281,6 +337,23 @@ def is_endpoint_allowed(
281
337
  return False, f"HTTP method {method} not allowed for {path}"
282
338
 
283
339
 
340
+ def get_endpoint_hint(endpoint: str) -> str:
341
+ """
342
+ Get hint for an endpoint if available.
343
+
344
+ Returns:
345
+ Hint string or empty string if no hint
346
+ """
347
+ parsed = urlparse(endpoint)
348
+ path = parsed.path
349
+
350
+ for pattern, hint in WHITELISTED_ENDPOINTS:
351
+ if re.match(pattern, path):
352
+ return hint
353
+
354
+ return ""
355
+
356
+
284
357
  class BaseDatadogGeneralTool(Tool):
285
358
  """Base class for general Datadog API tools."""
286
359
 
@@ -293,7 +366,7 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
293
366
  def __init__(self, toolset: "DatadogGeneralToolset"):
294
367
  super().__init__(
295
368
  name="datadog_api_get",
296
- description="Make a GET request to a Datadog API endpoint for read-only operations",
369
+ description="[datadog/general toolset] Make a GET request to a Datadog API endpoint for read-only operations",
297
370
  parameters={
298
371
  "endpoint": ToolParameter(
299
372
  description="The API endpoint path (e.g., '/api/v1/monitors', '/api/v2/events')",
@@ -301,7 +374,14 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
301
374
  required=True,
302
375
  ),
303
376
  "query_params": ToolParameter(
304
- description="Query parameters as a dictionary (e.g., {'from': '2024-01-01', 'to': '2024-01-02'})",
377
+ description="""Query parameters as a dictionary.
378
+ Time format requirements:
379
+ - v1 API: Unix timestamps in seconds (e.g., {'start': 1704067200, 'end': 1704153600})
380
+ - v2 API: RFC3339 format (e.g., {'from': '2024-01-01T00:00:00Z', 'to': '2024-01-02T00:00:00Z'})
381
+ - Relative times like '-24h', 'now', '-7d' will be auto-converted to proper format
382
+
383
+ Example for events: {'start': 1704067200, 'end': 1704153600}
384
+ Example for monitors: {'name': 'my-monitor', 'tags': 'env:prod'}""",
305
385
  type="object",
306
386
  required=False,
307
387
  ),
@@ -334,7 +414,7 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
334
414
 
335
415
  if not self.toolset.dd_config:
336
416
  return StructuredToolResult(
337
- status=ToolResultStatus.ERROR,
417
+ status=StructuredToolResultStatus.ERROR,
338
418
  error=TOOLSET_CONFIG_MISSING_ERROR,
339
419
  params=params,
340
420
  )
@@ -351,7 +431,7 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
351
431
  if not is_allowed:
352
432
  logging.error(f"Endpoint validation failed: {error_msg}")
353
433
  return StructuredToolResult(
354
- status=ToolResultStatus.ERROR,
434
+ status=StructuredToolResultStatus.ERROR,
355
435
  error=f"Endpoint validation failed: {error_msg}",
356
436
  params=params,
357
437
  )
@@ -366,11 +446,14 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
366
446
 
367
447
  logging.info(f"Full API URL: {url}")
368
448
 
449
+ # Preprocess time fields if any
450
+ processed_params = preprocess_time_fields(query_params, endpoint)
451
+
369
452
  # Execute request
370
453
  response = execute_datadog_http_request(
371
454
  url=url,
372
455
  headers=headers,
373
- payload_or_params=query_params,
456
+ payload_or_params=processed_params,
374
457
  timeout=self.toolset.dd_config.request_timeout,
375
458
  method="GET",
376
459
  )
@@ -382,13 +465,13 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
382
465
  > self.toolset.dd_config.max_response_size
383
466
  ):
384
467
  return StructuredToolResult(
385
- status=ToolResultStatus.ERROR,
468
+ status=StructuredToolResultStatus.ERROR,
386
469
  error=f"Response too large (>{self.toolset.dd_config.max_response_size} bytes)",
387
470
  params=params,
388
471
  )
389
472
 
390
473
  return StructuredToolResult(
391
- status=ToolResultStatus.SUCCESS,
474
+ status=StructuredToolResultStatus.SUCCESS,
392
475
  data=response_str,
393
476
  params=params,
394
477
  )
@@ -404,11 +487,16 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
404
487
  )
405
488
  elif e.status_code == 404:
406
489
  error_msg = f"Endpoint not found: {endpoint}"
490
+ elif e.status_code == 400:
491
+ # Use enhanced error message for 400 errors
492
+ error_msg = enhance_error_message(
493
+ e, endpoint, "GET", str(self.toolset.dd_config.site_api_url)
494
+ )
407
495
  else:
408
496
  error_msg = f"API error {e.status_code}: {str(e)}"
409
497
 
410
498
  return StructuredToolResult(
411
- status=ToolResultStatus.ERROR,
499
+ status=StructuredToolResultStatus.ERROR,
412
500
  error=error_msg,
413
501
  params=params,
414
502
  invocation=json.dumps({"url": url, "params": query_params})
@@ -419,7 +507,7 @@ class DatadogAPIGet(BaseDatadogGeneralTool):
419
507
  except Exception as e:
420
508
  logging.exception(f"Failed to query Datadog API: {params}", exc_info=True)
421
509
  return StructuredToolResult(
422
- status=ToolResultStatus.ERROR,
510
+ status=StructuredToolResultStatus.ERROR,
423
511
  error=f"Unexpected error: {str(e)}",
424
512
  params=params,
425
513
  )
@@ -431,7 +519,7 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
431
519
  def __init__(self, toolset: "DatadogGeneralToolset"):
432
520
  super().__init__(
433
521
  name="datadog_api_post_search",
434
- description="Make a POST request to Datadog search/query endpoints for complex filtering",
522
+ description="[datadog/general toolset] Make a POST request to Datadog search/query endpoints for complex filtering",
435
523
  parameters={
436
524
  "endpoint": ToolParameter(
437
525
  description="The search API endpoint (e.g., '/api/v2/monitor/search', '/api/v2/events/search')",
@@ -439,7 +527,29 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
439
527
  required=True,
440
528
  ),
441
529
  "body": ToolParameter(
442
- description="Request body for the search/filter operation",
530
+ description="""Request body for the search/filter operation.
531
+ Time format requirements:
532
+ - v1 API: Unix timestamps (e.g., 1704067200)
533
+ - v2 API: RFC3339 format (e.g., '2024-01-01T00:00:00Z')
534
+ - Relative times like '-24h', 'now', '-7d' will be auto-converted
535
+
536
+ Example for logs search:
537
+ {
538
+ "filter": {
539
+ "from": "2024-01-01T00:00:00Z",
540
+ "to": "2024-01-02T00:00:00Z",
541
+ "query": "*"
542
+ },
543
+ "sort": "-timestamp",
544
+ "page": {"limit": 50}
545
+ }
546
+
547
+ Example for monitor search:
548
+ {
549
+ "query": "env:production",
550
+ "page": 0,
551
+ "per_page": 20
552
+ }""",
443
553
  type="object",
444
554
  required=True,
445
555
  ),
@@ -470,7 +580,7 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
470
580
 
471
581
  if not self.toolset.dd_config:
472
582
  return StructuredToolResult(
473
- status=ToolResultStatus.ERROR,
583
+ status=StructuredToolResultStatus.ERROR,
474
584
  error=TOOLSET_CONFIG_MISSING_ERROR,
475
585
  params=params,
476
586
  )
@@ -487,7 +597,7 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
487
597
  if not is_allowed:
488
598
  logging.error(f"Endpoint validation failed: {error_msg}")
489
599
  return StructuredToolResult(
490
- status=ToolResultStatus.ERROR,
600
+ status=StructuredToolResultStatus.ERROR,
491
601
  error=f"Endpoint validation failed: {error_msg}",
492
602
  params=params,
493
603
  )
@@ -502,11 +612,14 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
502
612
 
503
613
  logging.info(f"Full API URL: {url}")
504
614
 
615
+ # Preprocess time fields if any
616
+ processed_body = preprocess_time_fields(body, endpoint)
617
+
505
618
  # Execute request
506
619
  response = execute_datadog_http_request(
507
620
  url=url,
508
621
  headers=headers,
509
- payload_or_params=body,
622
+ payload_or_params=processed_body,
510
623
  timeout=self.toolset.dd_config.request_timeout,
511
624
  method="POST",
512
625
  )
@@ -518,13 +631,13 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
518
631
  > self.toolset.dd_config.max_response_size
519
632
  ):
520
633
  return StructuredToolResult(
521
- status=ToolResultStatus.ERROR,
634
+ status=StructuredToolResultStatus.ERROR,
522
635
  error=f"Response too large (>{self.toolset.dd_config.max_response_size} bytes)",
523
636
  params=params,
524
637
  )
525
638
 
526
639
  return StructuredToolResult(
527
- status=ToolResultStatus.SUCCESS,
640
+ status=StructuredToolResultStatus.SUCCESS,
528
641
  data=response_str,
529
642
  params=params,
530
643
  )
@@ -540,11 +653,16 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
540
653
  )
541
654
  elif e.status_code == 404:
542
655
  error_msg = f"Endpoint not found: {endpoint}"
656
+ elif e.status_code == 400:
657
+ # Use enhanced error message for 400 errors
658
+ error_msg = enhance_error_message(
659
+ e, endpoint, "POST", str(self.toolset.dd_config.site_api_url)
660
+ )
543
661
  else:
544
662
  error_msg = f"API error {e.status_code}: {str(e)}"
545
663
 
546
664
  return StructuredToolResult(
547
- status=ToolResultStatus.ERROR,
665
+ status=StructuredToolResultStatus.ERROR,
548
666
  error=error_msg,
549
667
  params=params,
550
668
  invocation=json.dumps({"url": url, "body": body}) if url else None,
@@ -553,7 +671,7 @@ class DatadogAPIPostSearch(BaseDatadogGeneralTool):
553
671
  except Exception as e:
554
672
  logging.exception(f"Failed to query Datadog API: {params}", exc_info=True)
555
673
  return StructuredToolResult(
556
- status=ToolResultStatus.ERROR,
674
+ status=StructuredToolResultStatus.ERROR,
557
675
  error=f"Unexpected error: {str(e)}",
558
676
  params=params,
559
677
  )
@@ -565,10 +683,10 @@ class ListDatadogAPIResources(BaseDatadogGeneralTool):
565
683
  def __init__(self, toolset: "DatadogGeneralToolset"):
566
684
  super().__init__(
567
685
  name="list_datadog_api_resources",
568
- description="List available Datadog API resources and endpoints that can be accessed",
686
+ description="[datadog/general toolset] List available Datadog API resources and endpoints that can be accessed",
569
687
  parameters={
570
- "category": ToolParameter(
571
- description="Filter by category (e.g., 'monitors', 'dashboards', 'slos', 'incidents', 'synthetics', 'security', 'hosts', 'all')",
688
+ "search_regex": ToolParameter(
689
+ description="Optional regex pattern to filter endpoints (e.g., 'monitor', 'logs|metrics', 'security.*signals', 'v2/.*search$'). If not provided, shows all endpoints.",
572
690
  type="string",
573
691
  required=False,
574
692
  ),
@@ -578,145 +696,165 @@ class ListDatadogAPIResources(BaseDatadogGeneralTool):
578
696
 
579
697
  def get_parameterized_one_liner(self, params: dict) -> str:
580
698
  """Get a one-liner description of the tool invocation."""
581
- category = params.get("category", "all")
582
- return f"{toolset_name_for_one_liner(self.toolset.name)}: List API Resources ({category})"
699
+ search = params.get("search_regex", "all")
700
+ return f"{toolset_name_for_one_liner(self.toolset.name)}: List API Resources (search: {search})"
583
701
 
584
702
  def _invoke(
585
703
  self, params: dict, user_approved: bool = False
586
704
  ) -> StructuredToolResult:
587
705
  """List available API resources."""
588
- category = params.get("category", "all").lower()
706
+ search_regex = params.get("search_regex", "")
589
707
 
590
708
  logging.info("=" * 60)
591
709
  logging.info("ListDatadogAPIResources Tool Invocation:")
592
- logging.info(f" Category: {category}")
710
+ logging.info(f" Search regex: {search_regex or 'None (showing all)'}")
711
+ logging.info(f" OpenAPI Spec Loaded: {self.toolset.openapi_spec is not None}")
593
712
  logging.info("=" * 60)
594
713
 
595
- # Define categories and their endpoints
596
- resources = {
597
- "monitors": {
598
- "description": "Monitor management and alerting",
599
- "endpoints": [
600
- "GET /api/v1/monitor - List all monitors",
601
- "GET /api/v1/monitor/{id} - Get a monitor by ID",
602
- "POST /api/v1/monitor/search - Search monitors",
603
- "GET /api/v1/monitor/groups/search - Search monitor groups",
604
- ],
605
- },
606
- "dashboards": {
607
- "description": "Dashboard and visualization management",
608
- "endpoints": [
609
- "GET /api/v1/dashboard - List all dashboards",
610
- "GET /api/v1/dashboard/{id} - Get a dashboard by ID",
611
- "POST /api/v1/dashboard/lists - List dashboard lists",
612
- "GET /api/v1/dashboard/public/{token} - Get public dashboard",
613
- ],
614
- },
615
- "slos": {
616
- "description": "Service Level Objectives",
617
- "endpoints": [
618
- "GET /api/v1/slo - List all SLOs",
619
- "GET /api/v1/slo/{id} - Get an SLO by ID",
620
- "GET /api/v1/slo/{id}/history - Get SLO history",
621
- "POST /api/v1/slo/search - Search SLOs",
622
- "GET /api/v1/slo/{id}/corrections - Get SLO corrections",
623
- ],
624
- },
625
- "incidents": {
626
- "description": "Incident management",
627
- "endpoints": [
628
- "GET /api/v2/incidents - List incidents",
629
- "GET /api/v2/incidents/{id} - Get incident details",
630
- "POST /api/v2/incidents/search - Search incidents",
631
- "GET /api/v2/incidents/{id}/timeline - Get incident timeline",
632
- "GET /api/v2/incidents/{id}/attachments - Get incident attachments",
633
- ],
634
- },
635
- "synthetics": {
636
- "description": "Synthetic monitoring and testing",
637
- "endpoints": [
638
- "GET /api/v1/synthetics/tests - List synthetic tests",
639
- "GET /api/v1/synthetics/tests/{id} - Get test details",
640
- "POST /api/v1/synthetics/tests/search - Search tests",
641
- "GET /api/v1/synthetics/tests/{id}/results - Get test results",
642
- "GET /api/v1/synthetics/locations - List test locations",
643
- ],
644
- },
645
- "security": {
646
- "description": "Security monitoring and detection",
647
- "endpoints": [
648
- "GET /api/v2/security_monitoring/rules - List security rules",
649
- "GET /api/v2/security_monitoring/rules/{id} - Get rule details",
650
- "POST /api/v2/security_monitoring/rules/search - Search rules",
651
- "POST /api/v2/security_monitoring/signals/search - Search security signals",
652
- ],
653
- },
654
- "hosts": {
655
- "description": "Host and infrastructure monitoring",
656
- "endpoints": [
657
- "GET /api/v1/hosts - List all hosts",
658
- "GET /api/v1/hosts/{name} - Get host details",
659
- "GET /api/v1/hosts/totals - Get host totals",
660
- "GET /api/v1/tags/hosts - Get host tags",
661
- ],
662
- },
663
- "events": {
664
- "description": "Event stream and management",
665
- "endpoints": [
666
- "GET /api/v1/events - Query event stream",
667
- "GET /api/v1/events/{id} - Get event details",
668
- "POST /api/v2/events/search - Search events",
669
- ],
670
- },
671
- "usage": {
672
- "description": "Usage and billing information",
673
- "endpoints": [
674
- "GET /api/v1/usage/summary - Get usage summary",
675
- "GET /api/v1/usage/billable-summary - Get billable summary",
676
- "GET /api/v1/usage/estimated_cost - Get estimated costs",
677
- "GET /api/v2/usage/cost_by_org - Get costs by organization",
678
- ],
679
- },
680
- "services": {
681
- "description": "APM service information",
682
- "endpoints": [
683
- "GET /api/v2/services - List services",
684
- "GET /api/v2/services/{service} - Get service details",
685
- "GET /api/v2/services/{service}/dependencies - Get service dependencies",
686
- ],
687
- },
688
- }
714
+ # Filter endpoints based on regex search
715
+ matching_endpoints = []
689
716
 
690
- # Filter by category if specified
691
- if category != "all":
692
- matching_categories = {k: v for k, v in resources.items() if category in k}
693
- if not matching_categories:
717
+ if search_regex:
718
+ try:
719
+ search_pattern = re.compile(search_regex, re.IGNORECASE)
720
+ except re.error as e:
694
721
  return StructuredToolResult(
695
- status=ToolResultStatus.ERROR,
696
- error=f"Unknown category: {category}. Available: {', '.join(resources.keys())}",
722
+ status=StructuredToolResultStatus.ERROR,
723
+ error=f"Invalid regex pattern: {e}",
697
724
  params=params,
698
725
  )
699
- resources = matching_categories
726
+ else:
727
+ search_pattern = None
728
+
729
+ # Build list of matching endpoints
730
+ for pattern, hint in WHITELISTED_ENDPOINTS:
731
+ # Create a readable endpoint example from the pattern
732
+ example_endpoint = pattern.replace(r"^/api/v\d+", "/api/v1")
733
+ example_endpoint = example_endpoint.replace(r"(/search)?$", "")
734
+ example_endpoint = example_endpoint.replace(r"(/[^/]+)?$", "/{id}")
735
+ example_endpoint = example_endpoint.replace(r"/[^/]+$", "/{id}")
736
+ example_endpoint = example_endpoint.replace(r"/\d+$", "/{id}")
737
+ example_endpoint = example_endpoint.replace("$", "")
738
+ example_endpoint = example_endpoint.replace("^", "")
739
+
740
+ # Apply search filter if provided
741
+ if search_pattern and not search_pattern.search(example_endpoint):
742
+ continue
743
+
744
+ # Determine HTTP methods
745
+ if "search" in pattern or "query" in pattern or "aggregate" in pattern:
746
+ methods = "POST"
747
+ elif "/search)?$" in pattern:
748
+ methods = "GET/POST"
749
+ else:
750
+ methods = "GET"
700
751
 
701
- # Format output
702
- output = ["Available Datadog API Resources", "=" * 40, ""]
752
+ endpoint_info = {
753
+ "endpoint": example_endpoint,
754
+ "methods": methods,
755
+ "hint": hint,
756
+ "pattern": pattern,
757
+ }
758
+ matching_endpoints.append(endpoint_info)
703
759
 
704
- for cat_name, cat_info in resources.items():
705
- output.append(f"## {cat_name.upper()}")
706
- output.append(f"Description: {cat_info['description']}")
707
- output.append("")
708
- output.append("Endpoints:")
709
- for endpoint in cat_info["endpoints"]:
710
- output.append(f" • {endpoint}")
711
- output.append("")
760
+ if not matching_endpoints:
761
+ return StructuredToolResult(
762
+ status=StructuredToolResultStatus.SUCCESS,
763
+ data=f"No endpoints found matching regex: {search_regex}",
764
+ params=params,
765
+ )
712
766
 
767
+ # Format output
768
+ output = ["Available Datadog API Endpoints", "=" * 40]
769
+
770
+ if search_regex:
771
+ output.append(f"Filter: {search_regex}")
772
+ output.append(f"Found: {len(matching_endpoints)} endpoints")
773
+ output.append("")
774
+
775
+ # List endpoints with spec info if available
776
+ for info in matching_endpoints:
777
+ line = f"{info['methods']:8} {info['endpoint']}"
778
+ if info["hint"]:
779
+ line += f"\n {info['hint']}"
780
+
781
+ # Add OpenAPI spec info for this specific endpoint if available
782
+ if self.toolset.openapi_spec and "paths" in self.toolset.openapi_spec:
783
+ # Try to find matching path in OpenAPI spec
784
+ spec_path = None
785
+ for path in self.toolset.openapi_spec["paths"].keys():
786
+ if re.match(info["pattern"], path):
787
+ spec_path = path
788
+ break
789
+
790
+ if spec_path and spec_path in self.toolset.openapi_spec["paths"]:
791
+ path_spec = self.toolset.openapi_spec["paths"][spec_path]
792
+ # Add actual OpenAPI schema for the endpoint
793
+ for method in ["get", "post", "put", "delete"]:
794
+ if method in path_spec:
795
+ method_spec = path_spec[method]
796
+ line += f"\n\n OpenAPI Schema ({method.upper()}):"
797
+
798
+ # Add summary if available
799
+ if "summary" in method_spec:
800
+ line += f"\n Summary: {method_spec['summary']}"
801
+
802
+ # Add parameters if available
803
+ if "parameters" in method_spec:
804
+ line += "\n Parameters:"
805
+ for param in method_spec["parameters"]:
806
+ param_info = f"\n - {param.get('name', 'unknown')} ({param.get('in', 'unknown')})"
807
+ if param.get("required", False):
808
+ param_info += " [required]"
809
+ if "description" in param:
810
+ param_info += f": {param['description'][:100]}"
811
+ line += param_info
812
+
813
+ # Add request body schema if available
814
+ if "requestBody" in method_spec:
815
+ line += "\n Request Body:"
816
+ if "content" in method_spec["requestBody"]:
817
+ for content_type, content_spec in method_spec[
818
+ "requestBody"
819
+ ]["content"].items():
820
+ if "schema" in content_spec:
821
+ # Show a compact version of the schema
822
+ schema_str = json.dumps(
823
+ content_spec["schema"], indent=10
824
+ )[:500]
825
+ if (
826
+ len(json.dumps(content_spec["schema"]))
827
+ > 500
828
+ ):
829
+ schema_str += "..."
830
+ line += f"\n Content-Type: {content_type}"
831
+ line += f"\n Schema: {schema_str}"
832
+
833
+ # Add response schema sample if available
834
+ if "responses" in method_spec:
835
+ if "200" in method_spec["responses"]:
836
+ line += "\n Response (200):"
837
+ resp = method_spec["responses"]["200"]
838
+ if "description" in resp:
839
+ line += f"\n {resp['description']}"
840
+ break
841
+
842
+ output.append(line)
843
+
844
+ output.append("")
713
845
  output.append(
714
846
  "Note: All endpoints are read-only. Use the appropriate tool with the endpoint path."
715
847
  )
716
848
  output.append("Example: datadog_api_get with endpoint='/api/v1/monitors'")
849
+ output.append("")
850
+ output.append("Search examples:")
851
+ output.append(" • 'monitor' - find all monitor endpoints")
852
+ output.append(" • 'logs|metrics' - find logs OR metrics endpoints")
853
+ output.append(" • 'v2.*search$' - find all v2 search endpoints")
854
+ output.append(" • 'security.*signals' - find security signals endpoints")
717
855
 
718
856
  return StructuredToolResult(
719
- status=ToolResultStatus.SUCCESS,
857
+ status=StructuredToolResultStatus.SUCCESS,
720
858
  data="\n".join(output),
721
859
  params=params,
722
860
  )