From 937971af8df6f44e2878d56ca0f3b6146e0481df Mon Sep 17 00:00:00 2001 From: nick80 Date: Thu, 25 Sep 2025 13:51:55 +0700 Subject: [PATCH] Added support for Opensearch Dashboards --- composer.lock | 136 +++--- .../EmulateElastic/AddTemplateHandler.php | 7 +- src/Plugin/EmulateElastic/CatHandler.php | 26 +- .../EmulateElastic/InitKibanaHandler.php | 24 +- .../EmulateElastic/NodesInfoKibanaHandler.php | 4 +- .../OpenSearchDashboards/Handler.php | 362 ++++++++++++++ .../OpenSearchDashboards/Payload.php | 183 ++++++++ .../QueryMap/OpenSearchDashboards.php | 442 ++++++++++++++++++ .../QueryMapLoaderTrait.php | 65 +++ src/Plugin/EmulateElastic/Payload.php | 48 +- .../OpenSearchDashboardsTest.php | 96 ++++ 11 files changed, 1282 insertions(+), 111 deletions(-) create mode 100644 src/Plugin/EmulateElastic/OpenSearchDashboards/Handler.php create mode 100644 src/Plugin/EmulateElastic/OpenSearchDashboards/Payload.php create mode 100644 src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMap/OpenSearchDashboards.php create mode 100644 src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMapLoaderTrait.php mode change 100644 => 100755 src/Plugin/EmulateElastic/Payload.php create mode 100644 test/Plugin/EmulateElastic/OpenSearchDashboards/OpenSearchDashboardsTest.php diff --git a/composer.lock b/composer.lock index 529181da..623e486e 100644 --- a/composer.lock +++ b/composer.lock @@ -149,16 +149,16 @@ }, { "name": "composer/composer", - "version": "2.8.11", + "version": "2.8.12", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "00e1a3396eea67033775c4a49c772376f45acd73" + "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/00e1a3396eea67033775c4a49c772376f45acd73", - "reference": "00e1a3396eea67033775c4a49c772376f45acd73", + "url": "https://api.github.com/repos/composer/composer/zipball/3e38919bc9a2c3c026f2151b5e56d04084ce8f0b", + "reference": "3e38919bc9a2c3c026f2151b5e56d04084ce8f0b", "shasum": "" }, "require": { @@ -169,20 +169,20 @@ "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", - "justinrainbow/json-schema": "^6.3.1", + "justinrainbow/json-schema": "^6.5.1", "php": "^7.2.5 || ^8.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "react/promise": "^2.11 || ^3.3", + "react/promise": "^3.3", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.2", "seld/signal-handler": "^2.0", - "symfony/console": "^5.4.35 || ^6.3.12 || ^7.0.3", - "symfony/filesystem": "^5.4.35 || ^6.3.12 || ^7.0.3", - "symfony/finder": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/console": "^5.4.47 || ^6.4.25 || ^7.1.10", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.1.10", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.1.10", "symfony/polyfill-php73": "^1.24", "symfony/polyfill-php80": "^1.24", "symfony/polyfill-php81": "^1.24", - "symfony/process": "^5.4.35 || ^6.3.12 || ^7.0.3" + "symfony/process": "^5.4.47 || ^6.4.25 || ^7.1.10" }, "require-dev": { "phpstan/phpstan": "^1.11.8", @@ -190,7 +190,7 @@ "phpstan/phpstan-phpunit": "^1.4.0", "phpstan/phpstan-strict-rules": "^1.6.0", "phpstan/phpstan-symfony": "^1.4.0", - "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1" + "symfony/phpunit-bridge": "^6.4.25 || ^7.3.3" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -243,7 +243,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.8.11" + "source": "https://github.com/composer/composer/tree/2.8.12" }, "funding": [ { @@ -255,7 +255,7 @@ "type": "github" } ], - "time": "2025-08-21T09:29:39+00:00" + "time": "2025-09-19T11:41:59+00:00" }, { "name": "composer/metadata-minifier", @@ -630,16 +630,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.5.1", + "version": "6.5.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "b5ab21e431594897e5bb86343c01f140ba862c26" + "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/b5ab21e431594897e5bb86343c01f140ba862c26", - "reference": "b5ab21e431594897e5bb86343c01f140ba862c26", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ac0d369c09653cf7af561f6d91a705bc617a87b8", + "reference": "ac0d369c09653cf7af561f6d91a705bc617a87b8", "shasum": "" }, "require": { @@ -699,9 +699,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.5.1" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.5.2" }, - "time": "2025-08-29T10:58:11+00:00" + "time": "2025-09-09T09:42:27+00:00" }, { "name": "manticoresoftware/buddy-core", @@ -709,12 +709,12 @@ "source": { "type": "git", "url": "https://github.com/manticoresoftware/buddy-core.git", - "reference": "29cfb3da2f5133da8fb24d87f1cca5982370dd11" + "reference": "9fd1c76812288453e362394294109045abc42036" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/manticoresoftware/buddy-core/zipball/29cfb3da2f5133da8fb24d87f1cca5982370dd11", - "reference": "29cfb3da2f5133da8fb24d87f1cca5982370dd11", + "url": "https://api.github.com/repos/manticoresoftware/buddy-core/zipball/9fd1c76812288453e362394294109045abc42036", + "reference": "9fd1c76812288453e362394294109045abc42036", "shasum": "" }, "require": { @@ -753,7 +753,7 @@ "issues": "https://github.com/manticoresoftware/buddy-core/issues", "source": "https://github.com/manticoresoftware/buddy-core/tree/main" }, - "time": "2025-07-03T03:14:47+00:00" + "time": "2025-09-16T09:03:24+00:00" }, { "name": "manticoresoftware/manticoresearch-backup", @@ -981,16 +981,16 @@ }, { "name": "marc-mabe/php-enum", - "version": "v4.7.1", + "version": "v4.7.2", "source": { "type": "git", "url": "https://github.com/marc-mabe/php-enum.git", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", - "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", "shasum": "" }, "require": { @@ -1048,9 +1048,9 @@ ], "support": { "issues": "https://github.com/marc-mabe/php-enum/issues", - "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" }, - "time": "2024-11-28T04:54:44+00:00" + "time": "2025-09-14T11:18:39+00:00" }, { "name": "php-ds/php-ds", @@ -3522,16 +3522,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.28", + "version": "1.12.31", "source": { "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" + "url": "https://github.com/phpstan/phpstan-phar-composer-source.git", + "reference": "git1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a7630bb5311a41d13a2364634c78c5f4da250d53", + "reference": "a7630bb5311a41d13a2364634c78c5f4da250d53", "shasum": "" }, "require": { @@ -3576,7 +3576,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2025-09-24T15:58:55+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3899,16 +3899,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.25", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/049c011e01be805202d8eebedef49f769a8ec7b7", - "reference": "049c011e01be805202d8eebedef49f769a8ec7b7", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -3933,7 +3933,7 @@ "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", + "sebastian/exporter": "^4.0.8", "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", @@ -3982,7 +3982,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.25" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -4006,7 +4006,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:38:31+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "sebastian/cli-parser", @@ -4449,16 +4449,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -4514,15 +4514,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", @@ -5009,32 +5021,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.21.1", + "version": "8.22.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "2b801e950ae1cceb30bb3c0373141f553c99d3c3" + "reference": "1dd80bf3b93692bedb21a6623c496887fad05fec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/2b801e950ae1cceb30bb3c0373141f553c99d3c3", - "reference": "2b801e950ae1cceb30bb3c0373141f553c99d3c3", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/1dd80bf3b93692bedb21a6623c496887fad05fec", + "reference": "1dd80bf3b93692bedb21a6623c496887fad05fec", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.1.2", "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^2.3.0", - "squizlabs/php_codesniffer": "^3.13.2" + "squizlabs/php_codesniffer": "^3.13.4" }, "require-dev": { "phing/phing": "3.0.1|3.1.0", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.22", + "phpstan/phpstan": "2.1.24", "phpstan/phpstan-deprecation-rules": "2.0.3", "phpstan/phpstan-phpunit": "2.0.7", "phpstan/phpstan-strict-rules": "2.0.6", - "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.27|12.3.7" + "phpunit/phpunit": "9.6.8|10.5.48|11.4.4|11.5.36|12.3.10" }, "type": "phpcodesniffer-standard", "extra": { @@ -5058,7 +5070,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.21.1" + "source": "https://github.com/slevomat/coding-standard/tree/8.22.1" }, "funding": [ { @@ -5070,20 +5082,20 @@ "type": "tidelift" } ], - "time": "2025-08-31T13:32:28+00:00" + "time": "2025-09-13T08:53:30+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.2", + "version": "3.13.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ad545ea9c1b7d270ce0fc9cbfb884161cd706119", + "reference": "ad545ea9c1b7d270ce0fc9cbfb884161cd706119", "shasum": "" }, "require": { @@ -5154,7 +5166,7 @@ "type": "thanks_dev" } ], - "time": "2025-06-17T22:17:01+00:00" + "time": "2025-09-05T05:47:09+00:00" }, { "name": "swoole/ide-helper", diff --git a/src/Plugin/EmulateElastic/AddTemplateHandler.php b/src/Plugin/EmulateElastic/AddTemplateHandler.php index cb95969a..374f63f8 100644 --- a/src/Plugin/EmulateElastic/AddTemplateHandler.php +++ b/src/Plugin/EmulateElastic/AddTemplateHandler.php @@ -47,12 +47,7 @@ public function run(): Task { throw new \Exception('Cannot parse request'); } $patterns = json_encode($request['index_patterns']); - foreach (['index_patterns', 'settings', 'mappings'] as $removeProp) { - if (!isset($request[$removeProp])) { - continue; - } - unset($request[$removeProp]); - } + unset($request['index_patterns']); $content = json_encode($request); $templateTable = self::TEMPLATE_TABLE; diff --git a/src/Plugin/EmulateElastic/CatHandler.php b/src/Plugin/EmulateElastic/CatHandler.php index 65df3b55..6b4c3973 100644 --- a/src/Plugin/EmulateElastic/CatHandler.php +++ b/src/Plugin/EmulateElastic/CatHandler.php @@ -15,6 +15,7 @@ use Manticoresearch\Buddy\Core\Plugin\BaseHandlerWithClient; use Manticoresearch\Buddy\Core\Task\Task; use Manticoresearch\Buddy\Core\Task\TaskResult; + use RuntimeException; /** @@ -22,7 +23,7 @@ */ class CatHandler extends BaseHandlerWithClient { - const CAT_ENTITIES = ['templates']; + const CAT_ENTITIES = ['templates', 'plugins']; /** * Initialize the executor @@ -58,11 +59,10 @@ public function run(): Task { $catInfo = []; foreach ($queryResult[0]['data'] as $entityInfo) { - $catInfo[] = [ - 'name' => $entityInfo['name'], - 'order' => 0, - 'index_patterns' => simdjson_decode($entityInfo['patterns'], true), - ] + simdjson_decode($entityInfo['content'], true); + $catInfo[] = match ($entityTable) { + '_templates' => self::buildCatTemplateRow($entityInfo), + default => [], + }; } return TaskResult::raw($catInfo); @@ -72,4 +72,18 @@ public function run(): Task { $taskFn, [$this->payload, $this->manticoreClient] )->run(); } + + /** + * + * @param array{name:string,patterns:string,content:string} $entityInfo + * @return array + */ + private static function buildCatTemplateRow(array $entityInfo): array { + return [ + 'name' => $entityInfo['name'], + 'order' => 0, + 'index_patterns' => simdjson_decode($entityInfo['patterns'], true), + ] + simdjson_decode($entityInfo['content'], true); + } + } diff --git a/src/Plugin/EmulateElastic/InitKibanaHandler.php b/src/Plugin/EmulateElastic/InitKibanaHandler.php index 63e723b9..f2f6a0f3 100644 --- a/src/Plugin/EmulateElastic/InitKibanaHandler.php +++ b/src/Plugin/EmulateElastic/InitKibanaHandler.php @@ -11,6 +11,7 @@ namespace Manticoresearch\Buddy\Base\Plugin\EmulateElastic; +use Manticoresearch\Buddy\Core\Error\GenericError; use Manticoresearch\Buddy\Core\ManticoreSearch\Client as HTTPClient; use Manticoresearch\Buddy\Core\Task\Task; use Manticoresearch\Buddy\Core\Task\TaskResult; @@ -65,15 +66,20 @@ public function run(): Task { ], 'status' => 404, ]; - } else { - $resp = []; - foreach ($queryResult[0]['data'] as $entity) { - $resp[$entity['_index']] = [ - 'aliases' => [ - $alias => [], - ], - ] + simdjson_decode($entity['_source'], true); - } + $customError = GenericError::create('', false); + $customError->setResponseErrorBody($resp); + $customError->setResponseErrorCode(404); + + throw $customError; + } + + $resp = []; + foreach ($queryResult[0]['data'] as $entity) { + $resp[$entity['_index']] = [ + 'aliases' => [ + $alias => [], + ], + ] + simdjson_decode($entity['_source'], true); } return TaskResult::raw($resp); diff --git a/src/Plugin/EmulateElastic/NodesInfoKibanaHandler.php b/src/Plugin/EmulateElastic/NodesInfoKibanaHandler.php index 9c5990e0..8d78b1b1 100644 --- a/src/Plugin/EmulateElastic/NodesInfoKibanaHandler.php +++ b/src/Plugin/EmulateElastic/NodesInfoKibanaHandler.php @@ -24,6 +24,8 @@ */ class NodesInfoKibanaHandler extends BaseHandlerWithClient { + const DEFAULT_KIBANA_VERSION = '7.6.0'; + /** * Initialize the executor * @@ -58,7 +60,7 @@ public function run(): Task { 'publish_address' => "$ip:$port", ], 'ip' => $ip, - 'version' => '7.6.0', + 'version' => $settings->searchdKibanaVersionString ?? self::DEFAULT_KIBANA_VERSION, ], ], ] diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/Handler.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/Handler.php new file mode 100644 index 00000000..bb6c141e --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/Handler.php @@ -0,0 +1,362 @@ +payload, $this->manticoreClient] + )->run(); + } + + /** + * Handle search requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleSearch(Payload $payload, Client $manticoreClient): TaskResult { + $request = json_decode($payload->body, true); + if (!is_array($request)) { + $request = []; + } + + $query = 'SELECT * FROM ' . $payload->table; + + if (isset($request['query'])) { + $query .= ' WHERE MATCH(\'@* ' . addslashes(json_encode($request['query'])) . '\')'; + } + + $size = $request['size'] ?? 10; + $query .= ' LIMIT ' . (int)$size; + + $result = $manticoreClient->sendRequest($query); + $data = $result->getResult()[0]['data'] ?? []; + + // Build OpenSearch response format + $response = [ + 'took' => 0, + 'timed_out' => false, + '_shards' => [ + 'total' => 1, + 'successful' => 1, + 'skipped' => 0, + 'failed' => 0, + ], + 'hits' => [ + 'total' => [ + 'value' => count($data), + 'relation' => 'eq', + ], + 'max_score' => 1.0, + 'hits' => [], + ], + ]; + + foreach ($data as $row) { + $response['hits']['hits'][] = [ + '_index' => $payload->table, + '_type' => '_doc', + '_id' => $row['id'] ?? uniqid(), + '_score' => 1.0, + '_source' => $row, + ]; + } + + return TaskResult::raw($response); + } + + /** + * Handle cat requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleCat(Payload $payload, Client $manticoreClient): TaskResult { + $query = 'SHOW TABLES'; + $result = $manticoreClient->sendRequest($query); + $data = $result->getResult()[0]['data'] ?? []; + + $response = []; + foreach ($data as $row) { + $tableName = $row['Index'] ?? ''; + $response[] = $tableName . "\t" . "open\t" . "1\t" . "1\t" . "0\t" . "0\t" . "0b"; + } + + return TaskResult::raw(implode("\n", $response)); + } + + /** + * Handle count requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleCount(Payload $payload, Client $manticoreClient): TaskResult { + $query = 'SELECT COUNT(*) as count FROM ' . $payload->table; + $result = $manticoreClient->sendRequest($query); + $data = $result->getResult()[0]['data'] ?? []; + $count = $data[0]['count'] ?? 0; + + $response = [ + 'count' => (int)$count, + '_shards' => [ + 'total' => 1, + 'successful' => 1, + 'skipped' => 0, + 'failed' => 0, + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle license requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleLicense(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + 'license' => [ + 'status' => 'active', + 'uid' => '12345678-1234-1234-1234-123456789012', + 'type' => 'basic', + 'issue_date' => '2024-01-01T00:00:00.000Z', + 'issue_date_in_millis' => 1704067200000, + 'expiry_date' => '2025-01-01T00:00:00.000Z', + 'expiry_date_in_millis' => 1735689600000, + 'max_nodes' => 1000, + 'issued_to' => 'Manticore Search', + 'issuer' => 'Manticore Software', + 'start_date_in_millis' => 1704067200000, + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle nodes requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleNodes(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + '_nodes' => [ + 'total' => 1, + 'successful' => 1, + 'failed' => 0, + ], + 'cluster_name' => 'manticore-cluster', + 'nodes' => [ + 'node-1' => [ + 'name' => 'manticore-node', + 'transport_address' => '127.0.0.1:9300', + 'host' => '127.0.0.1', + 'ip' => '127.0.0.1', + 'version' => '8.0.0', + 'build_hash' => 'abcdef123456', + 'roles' => ['data', 'ingest', 'master'], + 'attributes' => [], + 'os' => [ + 'name' => 'Linux', + 'arch' => 'x86_64', + 'version' => '5.4.0', + ], + 'process' => [ + 'refresh_interval_in_millis' => 1000, + 'id' => 12345, + ], + 'jvm' => [ + 'pid' => 12345, + 'version' => '11.0.0', + 'vm_name' => 'OpenJDK 64-Bit Server VM', + 'vm_version' => '11.0.0', + 'vm_vendor' => 'Oracle Corporation', + ], + ], + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle xpack requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleXpack(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + 'build' => [ + 'hash' => 'abcdef123456', + 'date' => '2024-01-01T00:00:00.000Z', + ], + 'license' => [ + 'type' => 'basic', + 'mode' => 'basic', + 'status' => 'active', + ], + 'features' => [ + 'aggregations' => [ + 'available' => true, + 'enabled' => true, + ], + 'analytics' => [ + 'available' => true, + 'enabled' => true, + ], + 'ccr' => [ + 'available' => false, + 'enabled' => false, + ], + 'data_frame' => [ + 'available' => false, + 'enabled' => false, + ], + 'data_science' => [ + 'available' => false, + 'enabled' => false, + ], + 'graph' => [ + 'available' => false, + 'enabled' => false, + ], + 'ilm' => [ + 'available' => false, + 'enabled' => false, + ], + 'logstash' => [ + 'available' => false, + 'enabled' => false, + ], + 'ml' => [ + 'available' => false, + 'enabled' => false, + ], + 'monitoring' => [ + 'available' => false, + 'enabled' => false, + ], + 'rollup' => [ + 'available' => false, + 'enabled' => false, + ], + 'security' => [ + 'available' => false, + 'enabled' => false, + ], + 'sql' => [ + 'available' => true, + 'enabled' => true, + ], + 'watcher' => [ + 'available' => false, + 'enabled' => false, + ], + ], + 'tagline' => 'You know, for search', + ]; + + return TaskResult::raw($response); + } + + /** + * Handle OpenSearch Dashboards requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleOpenSearchDashboards(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + '_index' => $payload->table, + '_type' => '_doc', + '_id' => 'config:1.0.0', + '_version' => 1, + 'found' => true, + '_source' => [ + 'type' => 'config', + 'updated_at' => '2024-01-01T00:00:00.000Z', + 'config' => [ + 'buildNum' => 1000, + 'defaultIndex' => 'manticore-index', + ], + ], + ]; + + return TaskResult::raw($response); + } + + /** + * Handle default requests + * @param Payload $payload + * @param Client $manticoreClient + * @return TaskResult + */ + private static function handleDefault(Payload $payload, Client $manticoreClient): TaskResult { + $response = [ + 'acknowledged' => true, + 'status' => 'ok', + ]; + + return TaskResult::raw($response); + } +} \ No newline at end of file diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/Payload.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/Payload.php new file mode 100644 index 00000000..24e81bce --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/Payload.php @@ -0,0 +1,183 @@ + + */ +final class Payload extends BasePayload { + + /** @var string $table */ + public string $table = ''; + + /** @var string $body */ + public string $body = ''; + + /** @var string $path */ + public string $path; + + /** @var string $requestTarget */ + public static string $requestTarget; + + /** + * Get description for this plugin + * @return string + */ + public static function getInfo(): string { + return 'Emulates OpenSearch Dashboards queries and generates responses' + . ' as if they were made by OpenSearch'; + } + + /** + * @param Request $request + * @return static + */ + public static function fromRequest(Request $request): static { + $self = new static(); + $pathParts = explode('/', ltrim($request->path, '/')); + $self->path = $request->path; + self::detectRequestTarget($pathParts, $self); + + // Set body for search requests + if (static::$requestTarget === '_search' || + in_array(static::$requestTarget, ['_doc', '_create', '_update', '_bulk'])) { + $self->body = $request->payload; + } + + $self->table = self::extractTableFromPath($pathParts); + return $self; + } + + /** + * @param Request $request + * @return bool + */ + public static function hasMatch(Request $request): bool { + $pathParts = explode('/', ltrim($request->path, '/')); + $pathParts = array_filter($pathParts); + $pathParts = array_values($pathParts); + + if (empty($pathParts)) { + return false; + } + + $opensearchDashboardsEndpoints = [ + '.opensearch_dashboards', + '.opensearch_dashboards_task_manager', + '_search', + '_cat', + '_count', + '_license', + '_nodes', + '_xpack', + '_update_by_query', + 'metric', + 'config', + 'space', + 'index-pattern', + 'settings', + 'telemetry', + 'stats' + ]; + + // Check if the first path part matches any OpenSearch Dashboards endpoint + if (in_array($pathParts[0], $opensearchDashboardsEndpoints)) { + return true; + } + + if (isset($pathParts[1]) && in_array($pathParts[1], ['_doc', '_create', '_update', '_search', '_count'])) { + return true; + } + + if (isset($pathParts[1]) && $pathParts[1] === '_field_caps') { + return true; + } + + return false; + } + + /** + * Detect the request target from path parts + * @param array $pathParts + * @param Payload $self + * @return void + */ + protected static function detectRequestTarget(array $pathParts, Payload $self): void { + if (empty($pathParts)) { + static::$requestTarget = ''; + return; + } + + $firstPart = $pathParts[0]; + $secondPart = $pathParts[1] ?? ''; + + if (in_array($firstPart, ['.opensearch_dashboards', '.opensearch_dashboards_task_manager'])) { + static::$requestTarget = $firstPart; + return; + } + + if (in_array($firstPart, ['_cat', '_count', '_license', '_nodes', '_xpack', '_search'])) { + static::$requestTarget = $firstPart; + return; + } + + if (in_array($secondPart, ['_doc', '_create', '_update', '_search'])) { + static::$requestTarget = $secondPart; + return; + } + + if ($secondPart === '_field_caps') { + static::$requestTarget = '_field_caps'; + return; + } + + // Default to the first part + static::$requestTarget = $firstPart; + } + + /** + * Extract table name from path parts + * @param array $pathParts + * @return string + */ + protected static function extractTableFromPath(array $pathParts): string { + if (empty($pathParts)) { + return ''; + } + + if (in_array($pathParts[0], ['.opensearch_dashboards', '.opensearch_dashboards_task_manager'])) { + return $pathParts[0]; + } + + if (isset($pathParts[1]) && in_array($pathParts[1], ['_doc', '_create', '_update', '_search'])) { + return $pathParts[0]; + } + + if (isset($pathParts[1]) && $pathParts[1] === '_field_caps') { + return $pathParts[0]; + } + + return $pathParts[0]; + } + + /** + * Get handler class name for this payload + * @return string + */ + public function getHandlerClassName(): string { + return Handler::class; + } +} \ No newline at end of file diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMap/OpenSearchDashboards.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMap/OpenSearchDashboards.php new file mode 100644 index 00000000..92ad8162 --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMap/OpenSearchDashboards.php @@ -0,0 +1,442 @@ + [ + '_id' => 'space:default', + '_index' => '.opensearch_dashboards_2', + '_primary_term' => 1, + '_seq_no' => 0, + '_source' => [ + 'migrationVersion' => [ + 'space' => '6.6.0', + ], + 'references' => [], + 'space' => [ + '_reserved' => true, + 'color' => '#00bfb3', + 'description' => 'This is your default space!', + 'disabledFeatures' => [], + 'name' => 'Default', + ], + 'type' => 'space', + 'updated_at' => '2024-05-27T13:55:01.278Z', + ], + '_type' => '_doc', + '_version' => 1, + 'found' => true, + ], + '.opensearch_dashboards/_doc/index-pattern' => [ + '_index' => '.opensearch_dashboards_2', + '_primary_term' => 1, + '_seq_no' => 0, + '_shards' => [ + 'failed' => 0, + 'successful' => 1, + 'total' => 1, + ], + '_type' => '_doc', + '_version' => 1, + 'result' => 'created', + ], + '.opensearch_dashboards/_doc/config%3A1.0.0' => [ + '_id' => 'config:1.0.0', + '_index' => '.opensearch_dashboards_2', + '_primary_term' => 1, + '_seq_no' => 0, + '_source' => [ + 'config' => [ + 'buildNum' => 1000, + 'defaultIndex' => 'manticore-index', + ], + 'references' => [], + 'type' => 'config', + 'updated_at' => '2024-05-27T13:55:27.747Z', + ], + '_type' => '_doc', + '_version' => 2, + 'found' => true, + ], + '.opensearch_dashboards' => [ + 'mappings' => [ + '_meta' => [ + 'migrationMappingPropertyHashes' => [ + 'action' => 'c0c235fba02ebd2a2412bcda79009b58', + 'action_task_params' => 'a9d49f184ee89641044be0ca2950fa3a', + 'alert' => 'e588043a01d3d43477e7cad7efa0f5d8', + 'apm-indices' => '9bb9b2bf1fa636ed8619cbab5ce6a1dd', + 'apm-services-telemetry' => '07ee1939fa4302c62ddc052ec03fed90', + 'canvas-element' => '7390014e1091044523666d97247392fc', + 'canvas-workpad' => 'b0a1706d356228dbdcb4a17e6b9eb231', + 'config' => '87aca8fdb053154f11383fce3dbf3edf', + 'dashboard' => 'd00f614b29a80360e1190193fd333bab', + 'file-upload-telemetry' => '0ed4d3e1983d1217a30982630897092e', + 'graph-workspace' => 'cd7ba1330e6682e9cc00b78850874be1', + 'index-pattern' => '66eccb05066c5a89924f48a9e9736499', + 'infrastructure-ui-source' => 'ddc0ecb18383f6b26101a2fadb2dab0c', + 'inventory-view' => '84b320fd67209906333ffce261128462', + 'kql-telemetry' => 'd12a98a6f19a2d273696597547e064ee', + 'lens' => '21c3ea0763beb1ecb0162529706b88c5', + 'lens-ui-telemetry' => '509bfa5978586998e05f9e303c07a327', + 'map' => '23d7aa4a720d4938ccde3983f87bd58d', + 'maps-telemetry' => '268da3a48066123fc5baf35abaa55014', + 'metrics-explorer-view' => '53c5365793677328df0ccb6138bf3cdd', + 'migrationVersion' => '4a1746014a75ade3a714e1db5763276f', + 'ml-telemetry' => '257fd1d4b4fdbb9cb4b8a3b27da201e9', + 'namespace' => '2f4316de49999235636386fe51dc06c1', + 'query' => '11aaeb7f5f7fa5bb43f25e18ce26e7d9', + 'references' => '7997cf5a56cc02bdc9c93361bde732b0', + 'space' => '4a1746014a75ade3a714e1db5763276f', + 'telemetry' => '257fd1d4b4fdbb9cb4b8a3b27da201e9', + 'url' => '11aaeb7f5f7fa5bb43f25e18ce26e7d9', + 'visualization' => 'd00f614b29a80360e1190193fd333bab', + ], + ], + 'properties' => [ + 'action' => [ + 'properties' => [ + 'actionTypeId' => [ + 'type' => 'keyword', + ], + 'config' => [ + 'type' => 'object', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'secrets' => [ + 'type' => 'object', + ], + ], + ], + 'action_task_params' => [ + 'properties' => [ + 'actionId' => [ + 'type' => 'keyword', + ], + 'apiKey' => [ + 'type' => 'keyword', + ], + 'consumer' => [ + 'type' => 'keyword', + ], + 'executionId' => [ + 'type' => 'keyword', + ], + 'params' => [ + 'type' => 'object', + ], + 'relatedSavedObjects' => [ + 'properties' => [ + 'id' => [ + 'type' => 'keyword', + ], + 'namespace' => [ + 'type' => 'keyword', + ], + 'type' => [ + 'type' => 'keyword', + ], + 'typeId' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'source' => [ + 'type' => 'keyword', + ], + 'taskId' => [ + 'type' => 'keyword', + ], + ], + ], + 'alert' => [ + 'properties' => [ + 'actions' => [ + 'properties' => [ + 'actionRef' => [ + 'type' => 'keyword', + ], + 'actionTypeId' => [ + 'type' => 'keyword', + ], + 'group' => [ + 'type' => 'keyword', + ], + 'params' => [ + 'type' => 'object', + ], + ], + 'type' => 'object', + ], + 'alertTypeId' => [ + 'type' => 'keyword', + ], + 'apiKey' => [ + 'type' => 'keyword', + ], + 'apiKeyOwner' => [ + 'type' => 'keyword', + ], + 'consumer' => [ + 'type' => 'keyword', + ], + 'createdAt' => [ + 'type' => 'date', + ], + 'createdBy' => [ + 'type' => 'keyword', + ], + 'enabled' => [ + 'type' => 'boolean', + ], + 'executionStatus' => [ + 'properties' => [ + 'error' => [ + 'properties' => [ + 'message' => [ + 'type' => 'keyword', + ], + 'reason' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'lastExecutionDate' => [ + 'type' => 'date', + ], + 'status' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'meta' => [ + 'properties' => [ + 'versionApiKeyLastmodified' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'muteAll' => [ + 'type' => 'boolean', + ], + 'mutedInstanceIds' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'params' => [ + 'type' => 'object', + ], + 'schedule' => [ + 'properties' => [ + 'interval' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'scheduledTaskId' => [ + 'type' => 'keyword', + ], + 'tags' => [ + 'type' => 'keyword', + ], + 'throttle' => [ + 'type' => 'keyword', + ], + 'updatedAt' => [ + 'type' => 'date', + ], + 'updatedBy' => [ + 'type' => 'keyword', + ], + ], + ], + 'config' => [ + 'properties' => [ + 'buildNum' => [ + 'type' => 'long', + ], + 'defaultIndex' => [ + 'type' => 'keyword', + ], + ], + ], + 'dashboard' => [ + 'properties' => [ + 'hits' => [ + 'type' => 'long', + ], + 'kibanaSavedObjectMeta' => [ + 'properties' => [ + 'searchSourceJSON' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'optionsJSON' => [ + 'type' => 'keyword', + ], + 'panelsJSON' => [ + 'type' => 'keyword', + ], + 'refs' => [ + 'properties' => [ + 'id' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'type' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'title' => [ + 'type' => 'keyword', + ], + 'version' => [ + 'type' => 'long', + ], + ], + ], + 'index-pattern' => [ + 'properties' => [ + 'fieldFormatMap' => [ + 'type' => 'keyword', + ], + 'fields' => [ + 'type' => 'keyword', + ], + 'intervalName' => [ + 'type' => 'keyword', + ], + 'notExpandable' => [ + 'type' => 'boolean', + ], + 'references' => [ + 'type' => 'object', + ], + 'sourceFilters' => [ + 'type' => 'keyword', + ], + 'timeFieldName' => [ + 'type' => 'keyword', + ], + 'title' => [ + 'type' => 'keyword', + ], + ], + ], + 'space' => [ + 'properties' => [ + '_reserved' => [ + 'type' => 'boolean', + ], + 'color' => [ + 'type' => 'keyword', + ], + 'description' => [ + 'type' => 'keyword', + ], + 'disabledFeatures' => [ + 'type' => 'keyword', + ], + 'imageUrl' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + ], + ], + 'url' => [ + 'properties' => [ + 'accessCount' => [ + 'type' => 'long', + ], + 'accessDate' => [ + 'type' => 'date', + ], + 'createDate' => [ + 'type' => 'date', + ], + 'url' => [ + 'type' => 'keyword', + ], + ], + ], + 'visualization' => [ + 'properties' => [ + 'hits' => [ + 'type' => 'long', + ], + 'kibanaSavedObjectMeta' => [ + 'properties' => [ + 'searchSourceJSON' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'optionsJSON' => [ + 'type' => 'keyword', + ], + 'refs' => [ + 'properties' => [ + 'id' => [ + 'type' => 'keyword', + ], + 'name' => [ + 'type' => 'keyword', + ], + 'type' => [ + 'type' => 'keyword', + ], + ], + 'type' => 'object', + ], + 'savedSearchRefName' => [ + 'type' => 'keyword', + ], + 'title' => [ + 'type' => 'keyword', + ], + 'uiStateJSON' => [ + 'type' => 'keyword', + ], + 'version' => [ + 'type' => 'long', + ], + 'visState' => [ + 'type' => 'keyword', + ], + ], + ], + ], + ], + 'settings' => [ + 'index' => [ + 'number_of_shards' => 1, + 'number_of_replicas' => 0, + ], + ], + ], +]; \ No newline at end of file diff --git a/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMapLoaderTrait.php b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMapLoaderTrait.php new file mode 100644 index 00000000..c01f0222 --- /dev/null +++ b/src/Plugin/EmulateElastic/OpenSearchDashboards/QueryMapLoaderTrait.php @@ -0,0 +1,65 @@ + */ + private static array $queryMaps = []; + + /** + * Load query map for a specific path + * @param string $path + * @return array|null + */ + protected static function loadQueryMap(string $path): ?array { + if (empty(self::$queryMaps)) { + self::loadAllQueryMaps(); + } + + return self::$queryMaps[$path] ?? null; + } + + /** + * Load all query maps + * @return void + */ + private static function loadAllQueryMaps(): void { + $queryMapFiles = [ + __DIR__ . '/QueryMap/OpenSearchDashboards.php', + ]; + + foreach ($queryMapFiles as $file) { + if (file_exists($file)) { + $map = include $file; + if (is_array($map)) { + self::$queryMaps = array_merge(self::$queryMaps, $map); + } + } + } + } + + /** + * Get all loaded query maps + * @return array + */ + protected static function getAllQueryMaps(): array { + if (empty(self::$queryMaps)) { + self::loadAllQueryMaps(); + } + + return self::$queryMaps; + } +} \ No newline at end of file diff --git a/src/Plugin/EmulateElastic/Payload.php b/src/Plugin/EmulateElastic/Payload.php old mode 100644 new mode 100755 index 8db42e94..731815c4 --- a/src/Plugin/EmulateElastic/Payload.php +++ b/src/Plugin/EmulateElastic/Payload.php @@ -25,7 +25,7 @@ final class Payload extends BasePayload { // Endpoint position in Kibana request path const KIBANA_ENDPOINT_PATH_POS = [ - 0 => ['_aliases', '_alias', '_cat', '_field_caps', '_template'], + 0 => ['_aliases', '_alias', '_cat', '_field_caps', '_template', '_index_template'], 1 => ['_create', '_doc', '_update', '_field_caps'], ]; @@ -73,22 +73,6 @@ public static function fromRequest(Request $request): static { $self->path = $request->path; self::detectRequestTarget($pathParts, $self); switch (static::$requestTarget) { - case '_cat': - case '_count': - case '_license': - case '_nodes': - case '_xpack': - case '.kibana': - case '.kibana_task_manager': - case '_update_by_query': - case 'metric': - case 'config': - case 'space': - case 'index-pattern': - case 'settings': - case 'telemetry': - case 'stats': - break; case '_doc': static::$requestTarget .= '_' . strtolower($request->httpMethod); $self->body = $request->payload; @@ -107,6 +91,17 @@ public static function fromRequest(Request $request): static { $self->table = static::$requestTarget; $self->body = $request->payload; break; + case '_index_template': + case '_template': + if ($request->httpMethod !== 'PUT') { + // Need this to avoid sending the 404 response for Elasticdump's requests which causes its failure + $customError = InvalidNetworkRequestError::create('', true); + $customError->setResponseErrorCode(200); + throw $customError; + } + $self->table = end($pathParts); + $self->body = $request->payload; + break; case '_mapping': /** * @var array{ @@ -125,18 +120,17 @@ public static function fromRequest(Request $request): static { $self->table = $pathParts[0]; $self->body = $request->payload; break; - case '_template': - $self->table = end($pathParts); - $self->body = $request->payload; - break; default: - if ($pathParts[0] === '_index_template') { - // Need this to avoid sending the 404 response for Elasticdump's requests which causes its failure - $customError = InvalidNetworkRequestError::create('', true); - $customError->setResponseErrorCode(200); - throw $customError; + if (!in_array( + static::$requestTarget, + [ + '_cat', '_count', '_license', '_nodes', '_xpack', '.kibana', '.kibana_task_manager', + '_update_by_query', 'metric', 'config', 'space', 'index-pattern', 'settings', 'telemetry', + 'stats', + ] + )) { + throw new Exception("Unsupported request type in {$request->path}: " . static::$requestTarget); } - throw new Exception("Unsupported request type in {$request->path}: " . static::$requestTarget); } return $self; diff --git a/test/Plugin/EmulateElastic/OpenSearchDashboards/OpenSearchDashboardsTest.php b/test/Plugin/EmulateElastic/OpenSearchDashboards/OpenSearchDashboardsTest.php new file mode 100644 index 00000000..84f41905 --- /dev/null +++ b/test/Plugin/EmulateElastic/OpenSearchDashboards/OpenSearchDashboardsTest.php @@ -0,0 +1,96 @@ +path = '/test-index/_search'; + $searchRequest->payload = '{"query":{"match_all":{}}}'; + $this->assertTrue(Payload::hasMatch($searchRequest)); + + // Test cat requests + $catRequest = new Request(); + $catRequest->path = '/_cat/indices'; + $this->assertTrue(Payload::hasMatch($catRequest)); + + // Test count requests + $countRequest = new Request(); + $countRequest->path = '/test-index/_count'; + $this->assertTrue(Payload::hasMatch($countRequest)); + + // Test license requests + $licenseRequest = new Request(); + $licenseRequest->path = '/_license'; + $this->assertTrue(Payload::hasMatch($licenseRequest)); + + // Test nodes requests + $nodesRequest = new Request(); + $nodesRequest->path = '/_nodes'; + $this->assertTrue(Payload::hasMatch($nodesRequest)); + + // Test xpack requests + $xpackRequest = new Request(); + $xpackRequest->path = '/_xpack'; + $this->assertTrue(Payload::hasMatch($xpackRequest)); + + // Test OpenSearch Dashboards requests + $dashboardsRequest = new Request(); + $dashboardsRequest->path = '/.opensearch_dashboards/_doc/config:1.0.0'; + $this->assertTrue(Payload::hasMatch($dashboardsRequest)); + + // Test non-matching requests + $nonMatchingRequest = new Request(); + $nonMatchingRequest->path = '/some/other/path'; + $this->assertFalse(Payload::hasMatch($nonMatchingRequest)); + } + + /** + * Test payload creation from request + * @return void + */ + public function testFromRequest(): void { + $request = new Request(); + $request->path = '/test-index/_search'; + $request->payload = '{"query":{"match_all":{}},"size":10}'; + + $payload = Payload::fromRequest($request); + + $this->assertEquals('/test-index/_search', $payload->path); + $this->assertEquals('test-index', $payload->table); + $this->assertEquals('{"query":{"match_all":{}},"size":10}', $payload->body); + $this->assertEquals('_search', Payload::$requestTarget); + } + + /** + * Test plugin info + * @return void + */ + public function testGetInfo(): void { + $info = Payload::getInfo(); + $this->assertStringContainsString('OpenSearch Dashboards', $info); + $this->assertStringContainsString('OpenSearch', $info); + } +} \ No newline at end of file