diff --git a/docs/data-sources/builtin-toolsets/grafanadashboards.md b/docs/data-sources/builtin-toolsets/grafanadashboards.md index af4cfc2560..eda3897b0f 100644 --- a/docs/data-sources/builtin-toolsets/grafanadashboards.md +++ b/docs/data-sources/builtin-toolsets/grafanadashboards.md @@ -58,6 +58,16 @@ A [Grafana service account token](https://grafana.com/docs/grafana/latest/admini | grafana_get_home_dashboard | Get the home dashboard configuration | | grafana_get_dashboard_tags | List all tags used across dashboards for categorization | +### Controlling response size + +Large dashboards can produce very big JSON payloads. To keep tool outputs readable you can: + +- **Limit depth:** `max_depth: 1` returns only the first level of keys, replacing deeper structures with a truncated marker. +- **Filter with jsonpath:** `jsonpath: $.dashboard.panels[*].title` extracts just the panel titles. +- **Filter with jq:** `jq: .dashboard.panels[].id` extracts panel IDs using familiar jq syntax. + +Filters run before depth truncation, so you can combine them to precisely scope the response. + ## How it Works ### Dashboard Query Extraction diff --git a/holmes/plugins/toolsets/grafana/toolset_grafana.py b/holmes/plugins/toolsets/grafana/toolset_grafana.py index ba3813e8f3..f23f269294 100644 --- a/holmes/plugins/toolsets/grafana/toolset_grafana.py +++ b/holmes/plugins/toolsets/grafana/toolset_grafana.py @@ -18,6 +18,11 @@ build_headers, ) from holmes.plugins.toolsets.utils import toolset_name_for_one_liner +import jq # type: ignore +from jsonpath_ng import parse as jsonpath_parse # type: ignore +import logging + +logger = logging.getLogger(__name__) class GrafanaDashboardConfig(GrafanaConfig): @@ -124,6 +129,53 @@ def _make_grafana_request( ) +def _truncate_to_depth(value: Any, max_depth: Optional[int], current_depth: int = 0): + """Recursively truncate dictionaries/lists beyond the requested depth.""" + if max_depth is None or max_depth < 0: + return value + + if current_depth >= max_depth: + if isinstance(value, (dict, list)): + return f"...truncated at depth {max_depth}" + return value + + if isinstance(value, dict): + return { + key: _truncate_to_depth(sub_value, max_depth, current_depth + 1) + for key, sub_value in value.items() + } + if isinstance(value, list): + return [ + _truncate_to_depth(item, max_depth, current_depth + 1) for item in value + ] + + return value + + +def _apply_jsonpath_filter(data: Any, expression: str) -> Tuple[Optional[Any], Optional[str]]: + try: + jsonpath_expr = jsonpath_parse(expression) + matches = [match.value for match in jsonpath_expr.find(data)] + if len(matches) == 1: + return matches[0], None + return matches, None + except Exception as exc: # pragma: no cover - defensive + logger.debug("Failed to apply jsonpath filter", exc_info=exc) + return None, f"Invalid jsonpath expression: {exc}" + + +def _apply_jq_filter(data: Any, expression: str) -> Tuple[Optional[Any], Optional[str]]: + try: + compiled = jq.compile(expression) + results = compiled.input(data).all() + if len(results) == 1: + return results[0], None + return results, None + except Exception as exc: # pragma: no cover - defensive + logger.debug("Failed to apply jq filter", exc_info=exc) + return None, f"Invalid jq filter: {exc}" + + class SearchDashboards(BaseGrafanaTool): def __init__(self, toolset: GrafanaToolset): super().__init__( @@ -249,6 +301,21 @@ def __init__(self, toolset: GrafanaToolset): type="string", required=True, ), + "max_depth": ToolParameter( + description="Maximum nesting depth to return from the dashboard JSON (0 returns only top-level keys). Leave empty for full response.", + type="integer", + required=False, + ), + "jsonpath": ToolParameter( + description="Optional jsonpath expression to extract specific parts of the dashboard JSON.", + type="string", + required=False, + ), + "jq": ToolParameter( + description="Optional jq filter to apply to the dashboard JSON.", + type="string", + required=False, + ), }, ) @@ -256,13 +323,35 @@ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolRes uid = params["uid"] result = self._make_grafana_request(f"/api/dashboards/uid/{uid}", params) + data = result.data + if params.get("jsonpath"): + data, error = _apply_jsonpath_filter(data, params["jsonpath"]) + if error: + return StructuredToolResult( + status=StructuredToolResultStatus.ERROR, + error=error, + params=params, + url=result.url, + ) + if params.get("jq"): + data, error = _apply_jq_filter(data, params["jq"]) + if error: + return StructuredToolResult( + status=StructuredToolResultStatus.ERROR, + error=error, + params=params, + url=result.url, + ) + + data = _truncate_to_depth(data, params.get("max_depth")) + dashboard_url = _build_grafana_dashboard_url( self._toolset.grafana_config, uid=uid ) return StructuredToolResult( status=result.status, - data=result.data, + data=data, params=result.params, url=dashboard_url if dashboard_url else result.url, ) @@ -277,11 +366,48 @@ def __init__(self, toolset: GrafanaToolset): toolset=toolset, name="grafana_get_home_dashboard", description="Get the home dashboard using the /api/dashboards/home endpoint", - parameters={}, + parameters={ + "max_depth": ToolParameter( + description="Maximum nesting depth to return from the dashboard JSON (0 returns only top-level keys). Leave empty for full response.", + type="integer", + required=False, + ), + "jsonpath": ToolParameter( + description="Optional jsonpath expression to extract specific parts of the dashboard JSON.", + type="string", + required=False, + ), + "jq": ToolParameter( + description="Optional jq filter to apply to the dashboard JSON.", + type="string", + required=False, + ), + }, ) def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolResult: result = self._make_grafana_request("/api/dashboards/home", params) + data = result.data + if params.get("jsonpath"): + data, error = _apply_jsonpath_filter(data, params["jsonpath"]) + if error: + return StructuredToolResult( + status=StructuredToolResultStatus.ERROR, + error=error, + params=params, + url=result.url, + ) + if params.get("jq"): + data, error = _apply_jq_filter(data, params["jq"]) + if error: + return StructuredToolResult( + status=StructuredToolResultStatus.ERROR, + error=error, + params=params, + url=result.url, + ) + + data = _truncate_to_depth(data, params.get("max_depth")) config = self._toolset.grafana_config dashboard_url = None @@ -292,7 +418,7 @@ def _invoke(self, params: dict, context: ToolInvokeContext) -> StructuredToolRes return StructuredToolResult( status=result.status, - data=result.data, + data=data, params=result.params, url=dashboard_url if dashboard_url else None, ) diff --git a/poetry.lock b/poetry.lock index 1194df7344..fb6e6f9af7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "ag-ui-protocol" @@ -2513,6 +2513,108 @@ files = [ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] +[[package]] +name = "jq" +version = "1.10.0" +description = "jq is a lightweight and flexible JSON processor." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "jq-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9bba438d1813e537294e77f6f0ab3f4b23d3a0ae125187edf4827260a31341a0"}, + {file = "jq-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3eb6aed0d9882c43ae4c1757b72afc02063504f69d14eb12352c9b2813137c71"}, + {file = "jq-1.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c2a6a83f8b59dcb0b9a09f1e6b042e667923916767d0bee869e217d067f5f25"}, + {file = "jq-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:731fa11ce06276365c51fe2e23821d33acf6c66e501acfc4dd95507be840dd39"}, + {file = "jq-1.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd313711ad4b6158662a65e1de9a4e34e6f297cacaa2a563b1d7c37fd453770e"}, + {file = "jq-1.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:af859195c4a46adc52866cbc08a5d56fea86cbb8d18c9e02b95fea7d0b9c872d"}, + {file = "jq-1.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:801d01e2c933fa3da70ce18d73adb29c4fd07ebe0e2da3f39f79719357a60014"}, + {file = "jq-1.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6557f291f0b13db045b35401fa8b67b866fa1488e3a9703a1bcc5d156948c59"}, + {file = "jq-1.10.0-cp310-cp310-win32.whl", hash = "sha256:148a140c16c366c42c63e5a920dc8259ab62034c6f2c6b0f410df579fdf04654"}, + {file = "jq-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f8aa3f4eb6948be7b9b7c8eb19d4fbdaa2765227d21ea875898e2d20552ad749"}, + {file = "jq-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1363930e8efe63a76e6be93ffc6fea6d9201ba13a21f8a9c7943e9c6a4184cf7"}, + {file = "jq-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:850c99324fdb2e42a2056c27ec45af87b1bc764a14c94cdf011f6e21d885f032"}, + {file = "jq-1.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75aabeae63f36fe421c25cb793f5e166500400e443e7f6ce509261d06d4f8b5d"}, + {file = "jq-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b6e1f04da95c5057346954b24e65cb401cf9c64566e68c4263454717fcf464d"}, + {file = "jq-1.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ff2ac703b1bde9209f124aa7948012b77e93a40f858c24cf0bbd48f150c15e8"}, + {file = "jq-1.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:83ae246c191e6c5363cb7987af10c4c3071ec6995424eb749d225fbb985d9e47"}, + {file = "jq-1.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:307ed7ac72c03843af46face4ec1b8238f6d0d68f5a37aab3b55d178c329ad34"}, + {file = "jq-1.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ded278e64dad446667656f7144cefe20cea16bb57cf6912ef6d1ddf3bddc9861"}, + {file = "jq-1.10.0-cp311-cp311-win32.whl", hash = "sha256:5d5624d43c8597b06a4a2c5461d1577f29f23991472a5da88b742f7fa529c1d1"}, + {file = "jq-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:08bf55484a20955264358823049ff8deb671bb0025d51707ec591b5eb18a94d7"}, + {file = "jq-1.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe636cfa95b7027e7b43da83ecfd61431c0de80c3e0aa4946534b087149dcb4c"}, + {file = "jq-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:947fc7e1baaa7e95833b950e5a66b3e13a5cff028bff2d009b8c320124d9e69b"}, + {file = "jq-1.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9382f85a347623afa521c43f8f09439e68906fd5b3492016f969a29219796bb9"}, + {file = "jq-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c376aab525d0a1debe403d3bc2f19fda9473696a1eda56bafc88248fc4ae6e7e"}, + {file = "jq-1.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:206f230c67a46776f848858c66b9c377a8e40c2b16195552edd96fd7b45f9a52"}, + {file = "jq-1.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:06986456ebc95ccb9e9c2a1f0e842bc9d441225a554a9f9d4370ad95a19ac000"}, + {file = "jq-1.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d02c0be958ddb4d9254ff251b045df2f8ee5995137702eeab4ffa81158bcdbe0"}, + {file = "jq-1.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cf6fd2ebd2453e75ceef207d5a95a39fcbda371a9b8916db0bd42e8737a621"}, + {file = "jq-1.10.0-cp312-cp312-win32.whl", hash = "sha256:655d75d54a343944a9b011f568156cdc29ae0b35d2fdeefb001f459a4e4fc313"}, + {file = "jq-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d67c2653ae41eab48f8888c213c9e1807b43167f26ac623c9f3e00989d3edee"}, + {file = "jq-1.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b11d6e115ebad15d738d49932c3a8b9bb302b928e0fb79acc80987598d147a43"}, + {file = "jq-1.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df278904c5727dfe5bc678131a0636d731cd944879d890adf2fc6de35214b19b"}, + {file = "jq-1.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab4c1ec69fd7719fb1356e2ade7bd2b5a63d6f0eaf5a90fdc5c9f6145f0474ce"}, + {file = "jq-1.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd24dc21c8afcbe5aa812878251cfafa6f1dc6e1126c35d460cc7e67eb331018"}, + {file = "jq-1.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c0d3e89cd239c340c3a54e145ddf52fe63de31866cb73368d22a66bfe7e823f"}, + {file = "jq-1.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76710b280e4c464395c3d8e656b849e2704bd06e950a4ebd767860572bbf67df"}, + {file = "jq-1.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b11a56f1fb6e2985fd3627dbd8a0637f62b1a704f7b19705733d461dafa26429"}, + {file = "jq-1.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ac05ae44d9aa1e462329e1510e0b5139ac4446de650c7bdfdab226aafdc978ec"}, + {file = "jq-1.10.0-cp313-cp313-win32.whl", hash = "sha256:0bad90f5734e2fc9d09c4116ae9102c357a4d75efa60a85758b0ba633774eddb"}, + {file = "jq-1.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4ec3fbca80a9dfb5349cdc2531faf14dd832e1847499513cf1fc477bcf46a479"}, + {file = "jq-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:09e6ca3095a3be59353a262c75f680a0934ac03e81d10b78d7eabcb9fb746543"}, + {file = "jq-1.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e7b81b69700ad6003f6d068ae5432fa54169e2c5b15a1f9073400d83c0115a"}, + {file = "jq-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36a959b8cff3796b42f51a0be5fa37126ee66fc822e29620485a229f6c9baec6"}, + {file = "jq-1.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44bd6b9d03367a9e8a3f8f5c8343b572fbec9d148242f226e2a6f2eb459ba2b"}, + {file = "jq-1.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:48fb2cfe083942e370876e865fad3836aedc1b06ef30a2376e53ab35d6a7f728"}, + {file = "jq-1.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d47fe013dc22c82b425ae5358729a3d38de4097dda28d63f591c8bdd97bae6cb"}, + {file = "jq-1.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2886606f40f2127ed4bea2aa2d30653d485ed26075dd5b52fb933aa7ec7b23d3"}, + {file = "jq-1.10.0-cp38-cp38-win32.whl", hash = "sha256:f1f277fd820246f0d80da2ddd39b6d5ea99b266c067abce34f1ff50bd3358477"}, + {file = "jq-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:ab050dc7a6c204dde3a3d28e340f937e00cf69c8d3f7dd17e8c3ffef916784df"}, + {file = "jq-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db746ec4f05a6622bca5785f58fa322f04c493de61c6761cbe5a61218babe3d9"}, + {file = "jq-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bb9a6b5ff1e9d261ffae51aefbc66660bc1f5713339943aa95af7631062be163"}, + {file = "jq-1.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6690eda99a5be16f28c6931918769af0c2d066257d4efedc7c4108cfbf4e242f"}, + {file = "jq-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f4b28f04bb69eadb99c7ba3911246e6a200a886d82ae190a0af95037c427de6"}, + {file = "jq-1.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7278f1dfc49387551e670133b363a3368eeac74672dd4b520b7da4f8e803058"}, + {file = "jq-1.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28c16ca90940dfb8a2bd28b2d02cb2a546faa91db5b03f2cb71148b158fc098c"}, + {file = "jq-1.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f700d2aa1ef2b58c33df0a06ba58e68196cc9f81d1b6eb6baaa34e81fc0dbe6d"}, + {file = "jq-1.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf18f3031da9727245529e16ad13ab8859d73cfe0bc0e09a79696038658c25d5"}, + {file = "jq-1.10.0-cp39-cp39-win32.whl", hash = "sha256:31753d5b45b1806d1d7241a45cb262b1f3db8c0f1f4c618d5a02cbe227d2c436"}, + {file = "jq-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:c4648684034ba5b975be9b0099ca230ef088c14eeffb3357b522d9cba02a05ea"}, + {file = "jq-1.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:591d81677336551fd9cf3b5e23c1929ae3cd039a5c2d5fb8042870ed5372fd8c"}, + {file = "jq-1.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7f68716e8533294d2f5152e8659288091ea705778a00e066ed3b418ed724d81"}, + {file = "jq-1.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72658f31d5723e7b87eea929e81d5083fd4132636a9dcdbf56ba9ea0e30ecaa3"}, + {file = "jq-1.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:039e84e19306f94bf0d15291f6d358c4086c702de48e2309c3183fd866bf2785"}, + {file = "jq-1.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e985ded6dc2105707cb04e98ca6acbe6c24614824ed0a2fae442d2b2bc78fbc4"}, + {file = "jq-1.10.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:185d450fb45cd44ad5939ec5813f1ed0d057fa0cb12a1ba6a5fe0e49d8354958"}, + {file = "jq-1.10.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e6649eb4ce390342e07c95c2fa10fed043410408620296122f0ac48a7576f1f"}, + {file = "jq-1.10.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:002d93e50fab9d92035dfd79fd134052692be649e1b3835662a053016d9f2ee7"}, + {file = "jq-1.10.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1602f95ffaef7ee357b65f414b59d6284619bd3a0a588c15c3a1ae534811c1fb"}, + {file = "jq-1.10.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b28cfd1eac69e1dc14518ed9c33e497041e9f9310e5f6259fa9d18fe342fb50"}, + {file = "jq-1.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:061225bc6b45b399f4dfbece00f4fae78560c1d4e0f2af77203dde621c5f10be"}, + {file = "jq-1.10.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dba7abfe55efe3f139247a30e1f13e94f33fddfea71245a18a817b619cb9fe9"}, + {file = "jq-1.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c6f45acaad61c1947bf2fa172a2ccb2e882231c3cfbfc1ea4a2c4f032122a546"}, + {file = "jq-1.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16c250cd0a708d45b9bb08fdf4cac415156274f7f3f8f026e75b5a330d2162dd"}, + {file = "jq-1.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:593551afc5f305c7d0adc840587350cb49c4ecba6464f4fc965cae87758621a7"}, + {file = "jq-1.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c550705145480b616b6bc114ab7f421c0d9a3041ad3dcb9424f992954823f7c2"}, + {file = "jq-1.10.0.tar.gz", hash = "sha256:fc38803075dbf1867e1b4ed268fef501feecb0c50f3555985a500faedfa70f08"}, +] + +[[package]] +name = "jsonpath-ng" +version = "1.7.0" +description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"}, + {file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"}, + {file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"}, +] + +[package.dependencies] +ply = "*" + [[package]] name = "jsonschema" version = "4.25.1" @@ -3784,6 +3886,18 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "ply" +version = "3.11" +description = "Python Lex & Yacc" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, + {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, +] + [[package]] name = "polyleven" version = "0.9.0" @@ -6215,4 +6329,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "443b859de7e966f4e184f36679ff5f7e1d1f6a38514bbd421f90f5995cecd7d6" +content-hash = "d175a137cdd723745a7144633f27b850c1d6aa3b1cb5ff4b6b1b420591992d85" diff --git a/pyproject.toml b/pyproject.toml index bda9fa1584..8e210f72bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,8 @@ prometrix = "0.2.5" httpx = {version = "<0.28", extras = ["socks"]} ag-ui-protocol = "^0.1.9" google-cloud-aiplatform = ">=1.38" +jq = "^1.8.0" +jsonpath-ng = "^1.6.1" [tool.poetry.group.dev] optional = true diff --git a/tests/plugins/toolsets/grafana/test_dashboard_filters.py b/tests/plugins/toolsets/grafana/test_dashboard_filters.py new file mode 100644 index 0000000000..db4964dc4b --- /dev/null +++ b/tests/plugins/toolsets/grafana/test_dashboard_filters.py @@ -0,0 +1,70 @@ +import pytest + +from holmes.core.tools import StructuredToolResult, StructuredToolResultStatus +from holmes.plugins.toolsets.grafana.toolset_grafana import ( + GrafanaDashboardConfig, + GrafanaToolset, + GetDashboardByUID, + _truncate_to_depth, +) + + +def _build_tool(data): + toolset = GrafanaToolset() + toolset._grafana_config = GrafanaDashboardConfig(url="http://example.com") + tool = GetDashboardByUID(toolset) + tool._make_grafana_request = lambda endpoint, params: StructuredToolResult( + status=StructuredToolResultStatus.SUCCESS, + data=data, + params=params, + url="http://api", + ) + return tool + + +def test_truncate_to_depth_limits_nested_values(): + data = {"a": {"b": {"c": 1}}, "list": [1, {"nested": 2}]} + truncated = _truncate_to_depth(data, 1) + assert truncated["a"] == "...truncated at depth 1" + assert truncated["list"] == "...truncated at depth 1" + + +def test_jsonpath_filter_applies_before_returning_data(): + data = {"dashboard": {"panels": [{"id": 1, "title": "CPU"}]}} + tool = _build_tool(data) + + result = tool._invoke( + {"uid": "abc", "jsonpath": "$.dashboard.panels[*].title"}, context=None + ) + + assert result.status is StructuredToolResultStatus.SUCCESS + assert result.data == "CPU" + + +def test_jq_filter_applies_before_returning_data(): + data = {"dashboard": {"panels": [{"id": 1, "title": "CPU"}]}} + tool = _build_tool(data) + + result = tool._invoke({"uid": "abc", "jq": ".dashboard.panels[].id"}, context=None) + + assert result.status is StructuredToolResultStatus.SUCCESS + assert result.data == 1 + + +def test_invalid_jsonpath_returns_error(): + data = {"dashboard": {"panels": [{"id": 1, "title": "CPU"}]}} + tool = _build_tool(data) + + result = tool._invoke({"uid": "abc", "jsonpath": "$.["}, context=None) + + assert result.status is StructuredToolResultStatus.ERROR + assert "Invalid jsonpath expression" in result.error + + +def test_depth_applies_after_filters(): + data = {"dashboard": {"panels": [{"id": 1, "title": "CPU"}]}} + tool = _build_tool(data) + + result = tool._invoke({"uid": "abc", "max_depth": 1}, context=None) + + assert result.data["dashboard"] == "...truncated at depth 1"