From cce36ae058ff3d91fbac600ffd5a48a55473e36e Mon Sep 17 00:00:00 2001 From: JcEvoX Date: Thu, 13 Aug 2026 20:11:52 +0800 Subject: [PATCH] feat(elasticsearch): allow disabling connectivity checks --- .../connection/ConnectionDialog.vue | 46 ++++++- apps/desktop/src/i18n/locales/en.ts | 2 + apps/desktop/src/i18n/locales/es.ts | 2 + apps/desktop/src/i18n/locales/it.ts | 2 + apps/desktop/src/i18n/locales/ja.ts | 2 + apps/desktop/src/i18n/locales/ko.ts | 2 + apps/desktop/src/i18n/locales/pt-BR.ts | 2 + apps/desktop/src/i18n/locales/zh-CN.ts | 2 + apps/desktop/src/i18n/locales/zh-TW.ts | 2 + .../elasticsearchKibanaProxy.spec.ts | 20 +++ .../connection/elasticsearchKibanaProxy.ts | 16 ++- .../dbx-core/src/db/elasticsearch_driver.rs | 114 +++++++++++++++++- 12 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/connection/elasticsearchKibanaProxy.spec.ts diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 9117f12de3..3f196e2fc6 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -108,7 +108,15 @@ import { oceanbaseModeConnectionPatch, oceanbaseSubModeFromConfig } from "@/lib/ import { translateBackendError } from "@/i18n/backend-errors"; import { applyHiveKerberosSubmitConfig, hiveKerberosFormConfig, type HiveKerberosAuthMode } from "@/lib/database/hiveKerberosOptions"; import { hasCloudflareD1Credentials, isCloudflareD1Connection, normalizeCloudflareD1Connection } from "@/lib/connection/cloudflareD1"; -import { buildElasticsearchExternalConfig, elasticsearchConnectionModeFromConfig, elasticsearchConnectivityCheckPathFromConfig, elasticsearchIndexGroupingPatternFromConfig, elasticsearchKibanaBasePathFromConfig, type ElasticsearchConnectionMode } from "@/lib/connection/elasticsearchKibanaProxy"; +import { + buildElasticsearchExternalConfig, + elasticsearchConnectionModeFromConfig, + elasticsearchConnectivityCheckDisabledFromConfig, + elasticsearchConnectivityCheckPathFromConfig, + elasticsearchIndexGroupingPatternFromConfig, + elasticsearchKibanaBasePathFromConfig, + type ElasticsearchConnectionMode, +} from "@/lib/connection/elasticsearchKibanaProxy"; import { GAUSSDB_M_JDBC_DRIVER_CLASS, gaussdbConnectionMode, gaussdbIdentifierQuoteStyle, setGaussdbConnectionMode, setGaussdbIdentifierQuoteStyle, supportsGaussdbIdentifierQuoteStyle, type GaussdbConnectionMode, type GaussdbIdentifierQuoteStyle } from "@/lib/database/jdbcDialect"; import { normalizeStoredConnectionDatabase } from "@/lib/database/sqliteNamespace"; import { @@ -327,6 +335,7 @@ const defaultForm = (): ConnectionForm => ({ const elasticsearchConnectionMode = ref("direct"); const elasticsearchKibanaBasePath = ref(""); const elasticsearchConnectivityCheckPath = ref(""); +const elasticsearchConnectivityCheckDisabled = ref(false); const elasticsearchIndexGroupingPattern = ref(""); const elasticsearchConnectionPorts = ref>({ direct: 9200, @@ -338,6 +347,7 @@ function resetElasticsearchProxyFields(externalConfig?: unknown) { elasticsearchConnectionMode.value = mode; elasticsearchKibanaBasePath.value = elasticsearchKibanaBasePathFromConfig(externalConfig); elasticsearchConnectivityCheckPath.value = elasticsearchConnectivityCheckPathFromConfig(externalConfig); + elasticsearchConnectivityCheckDisabled.value = elasticsearchConnectivityCheckDisabledFromConfig(externalConfig); elasticsearchIndexGroupingPattern.value = elasticsearchIndexGroupingPatternFromConfig(externalConfig); elasticsearchConnectionPorts.value = { direct: mode === "direct" ? form.value.port : 9200, @@ -3837,7 +3847,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo config.database = "metrics"; config.username = config.username.trim(); } else if (config.db_type === "elasticsearch") { - config.external_config = buildElasticsearchExternalConfig(elasticsearchConnectionMode.value, elasticsearchKibanaBasePath.value, elasticsearchConnectivityCheckPath.value, elasticsearchIndexGroupingPattern.value); + config.external_config = buildElasticsearchExternalConfig(elasticsearchConnectionMode.value, elasticsearchKibanaBasePath.value, elasticsearchConnectivityCheckPath.value, elasticsearchIndexGroupingPattern.value, elasticsearchConnectivityCheckDisabled.value); } else if (config.db_type === "meilisearch") { config.username = ""; config.password = config.password.trim(); @@ -6917,11 +6927,18 @@ function openExternalUrl(url: string) {
- -
- -

{{ t("connection.elasticsearchIndexGroupingPatternHint") }}

+
+ + + + + + + {{ t("connection.elasticsearchIndexGroupingPatternHint") }} + +
+
@@ -7582,6 +7599,23 @@ function openExternalUrl(url: string) {
+
+
+ + + + + + + {{ t("connection.elasticsearchConnectivityCheckDisabledHint") }} + + +
+
+ +
+
+
{{ t("nacos.nacosAdvancedTitle") }}
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 629f9a0074..d7650cd384 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -477,6 +477,8 @@ export default { elasticsearchKibanaBasePath: "Base Path", elasticsearchConnectivityCheckPath: "Connectivity Path", elasticsearchConnectivityCheckPathPlaceholder: "/ or /my-index/_search", + elasticsearchConnectivityCheckDisabled: "Disable connectivity check", + elasticsearchConnectivityCheckDisabledHint: "When on, connect and test skip the probe request and treat the server as reachable. Use it when the account lacks permission for the root path and any check path, or when permissions differ across clusters.", elasticsearchIndexGroupingPattern: "Index grouping regex", elasticsearchIndexGroupingPatternPlaceholder: "off by default; enter a regex to enable", elasticsearchIndexGroupingPatternHint: "Off by default (naming schemes vary). Enter a regular expression to enable: matched rolling or time-series indices are aggregated into one queryable wildcard pattern, hiding per-day detail.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 9105542373..102f3e337e 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -986,6 +986,8 @@ export default withEnglishFallback({ elasticsearchKibanaBasePath: "ruta base", elasticsearchConnectivityCheckPath: "Ruta de conectividad", elasticsearchConnectivityCheckPathPlaceholder: "/ o /my-index/_search", + elasticsearchConnectivityCheckDisabled: "Disable connectivity check", + elasticsearchConnectivityCheckDisabledHint: "When on, connect and test skip the probe request and treat the server as reachable. Use it when the account lacks permission for the root path and any check path, or when permissions differ across clusters.", elasticsearchIndexGroupingPattern: "Index grouping regex", elasticsearchIndexGroupingPatternPlaceholder: "off by default; enter a regex to enable", elasticsearchIndexGroupingPatternHint: "Off by default (naming schemes vary). Enter a regular expression to enable: matched rolling or time-series indices are aggregated into one queryable wildcard pattern, hiding per-day detail.", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 9f38f1b61c..d499c79a47 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -984,6 +984,8 @@ export default withEnglishFallback({ elasticsearchKibanaBasePath: "Percorso base", elasticsearchConnectivityCheckPath: "Percorso di connettività", elasticsearchConnectivityCheckPathPlaceholder: "/ o /my-index/_search", + elasticsearchConnectivityCheckDisabled: "Disable connectivity check", + elasticsearchConnectivityCheckDisabledHint: "When on, connect and test skip the probe request and treat the server as reachable. Use it when the account lacks permission for the root path and any check path, or when permissions differ across clusters.", elasticsearchIndexGroupingPattern: "Index grouping regex", elasticsearchIndexGroupingPatternPlaceholder: "off by default; enter a regex to enable", elasticsearchIndexGroupingPatternHint: "Off by default (naming schemes vary). Enter a regular expression to enable: matched rolling or time-series indices are aggregated into one queryable wildcard pattern, hiding per-day detail.", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 5a30f55d42..ddce095cc2 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -984,6 +984,8 @@ export default withEnglishFallback({ elasticsearchKibanaBasePath: "ベースパス", elasticsearchConnectivityCheckPath: "接続確認パス", elasticsearchConnectivityCheckPathPlaceholder: "/ または /my-index/_search", + elasticsearchConnectivityCheckDisabled: "Disable connectivity check", + elasticsearchConnectivityCheckDisabledHint: "When on, connect and test skip the probe request and treat the server as reachable. Use it when the account lacks permission for the root path and any check path, or when permissions differ across clusters.", elasticsearchIndexGroupingPattern: "Index grouping regex", elasticsearchIndexGroupingPatternPlaceholder: "off by default; enter a regex to enable", elasticsearchIndexGroupingPatternHint: "Off by default (naming schemes vary). Enter a regular expression to enable: matched rolling or time-series indices are aggregated into one queryable wildcard pattern, hiding per-day detail.", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index dc7bc8c032..dccdbc59ed 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -471,6 +471,8 @@ export default withEnglishFallback({ elasticsearchKibanaBasePath: "기본 경로", elasticsearchConnectivityCheckPath: "연결 확인 경로", elasticsearchConnectivityCheckPathPlaceholder: "/ 또는 /my-index/_search", + elasticsearchConnectivityCheckDisabled: "Disable connectivity check", + elasticsearchConnectivityCheckDisabledHint: "When on, connect and test skip the probe request and treat the server as reachable. Use it when the account lacks permission for the root path and any check path, or when permissions differ across clusters.", elasticsearchIndexGroupingPattern: "Index grouping regex", elasticsearchIndexGroupingPatternPlaceholder: "off by default; enter a regex to enable", elasticsearchIndexGroupingPatternHint: "Off by default (naming schemes vary). Enter a regular expression to enable: matched rolling or time-series indices are aggregated into one queryable wildcard pattern, hiding per-day detail.", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 6afc0ef777..7b7a865177 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -985,6 +985,8 @@ export default withEnglishFallback({ elasticsearchKibanaBasePath: "Caminho Base", elasticsearchConnectivityCheckPath: "Caminho de conectividade", elasticsearchConnectivityCheckPathPlaceholder: "/ ou /my-index/_search", + elasticsearchConnectivityCheckDisabled: "Disable connectivity check", + elasticsearchConnectivityCheckDisabledHint: "When on, connect and test skip the probe request and treat the server as reachable. Use it when the account lacks permission for the root path and any check path, or when permissions differ across clusters.", elasticsearchIndexGroupingPattern: "Index grouping regex", elasticsearchIndexGroupingPatternPlaceholder: "off by default; enter a regex to enable", elasticsearchIndexGroupingPatternHint: "Off by default (naming schemes vary). Enter a regular expression to enable: matched rolling or time-series indices are aggregated into one queryable wildcard pattern, hiding per-day detail.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index a5a4956fd0..487d57a624 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -402,6 +402,8 @@ export default withEnglishFallback({ elasticsearchKibanaBasePath: "基础路径", elasticsearchConnectivityCheckPath: "连通性检查路径", elasticsearchConnectivityCheckPathPlaceholder: "/ 或 /my-index/_search", + elasticsearchConnectivityCheckDisabled: "关闭连通性检查", + elasticsearchConnectivityCheckDisabledHint: "开启后连接与测试都不发探活请求,直接视为可连。用于账号对根路径和任何检查路径都无权限、或集群间权限不统一的情况。", elasticsearchIndexGroupingPattern: "索引聚合正则", elasticsearchIndexGroupingPatternPlaceholder: "默认关闭,填正则表达式开启", elasticsearchIndexGroupingPatternHint: "默认关闭(命名格式因环境而异)。填入正则表达式即可开启:将匹配到的滚动或时间序列索引聚合成一个可查询的通配 pattern,隐藏每日明细。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 41a55aa804..2cf5f5d6d2 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -984,6 +984,8 @@ export default withEnglishFallback({ elasticsearchKibanaBasePath: "基礎路徑", elasticsearchConnectivityCheckPath: "連通性檢查路徑", elasticsearchConnectivityCheckPathPlaceholder: "/ 或 /my-index/_search", + elasticsearchConnectivityCheckDisabled: "關閉連通性檢查", + elasticsearchConnectivityCheckDisabledHint: "開啟後連線與測試都不發探活請求,直接視為可連。用於帳號對根路徑和任何檢查路徑都無權限、或叢集間權限不統一的情況。", elasticsearchIndexGroupingPattern: "索引聚合正則", elasticsearchIndexGroupingPatternPlaceholder: "預設關閉,填正則表達式開啟", elasticsearchIndexGroupingPatternHint: "預設關閉(命名格式因環境而異)。填入正則表達式即可開啟:將比對到的滾動或時間序列索引聚合成一個可查詢的通配 pattern,隱藏每日明細。", diff --git a/apps/desktop/src/lib/__tests__/connection/elasticsearchKibanaProxy.spec.ts b/apps/desktop/src/lib/__tests__/connection/elasticsearchKibanaProxy.spec.ts new file mode 100644 index 0000000000..ad481df74e --- /dev/null +++ b/apps/desktop/src/lib/__tests__/connection/elasticsearchKibanaProxy.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { buildElasticsearchExternalConfig, elasticsearchConnectivityCheckDisabledFromConfig } from "@/lib/connection/elasticsearchKibanaProxy"; + +describe("Elasticsearch connectivity-check configuration", () => { + it.each([true, "true", "TRUE", "1", "yes", "YES", "on", "ON"])("reads enabled value %j", (value) => { + expect(elasticsearchConnectivityCheckDisabledFromConfig({ connectivityCheckDisabled: value })).toBe(true); + }); + + it.each([false, "false", "0", "no", "off", "", 1, null])("rejects disabled value %j", (value) => { + expect(elasticsearchConnectivityCheckDisabledFromConfig({ connectivityCheckDisabled: value })).toBe(false); + }); + + it("normalizes a legacy string flag when an edited connection is saved", () => { + const disabled = elasticsearchConnectivityCheckDisabledFromConfig({ connectivityCheckDisabled: " yes " }); + const saved = buildElasticsearchExternalConfig("direct", "", "", "", disabled); + + expect(saved).toEqual({ connectivityCheckDisabled: true }); + expect(elasticsearchConnectivityCheckDisabledFromConfig(saved)).toBe(true); + }); +}); diff --git a/apps/desktop/src/lib/connection/elasticsearchKibanaProxy.ts b/apps/desktop/src/lib/connection/elasticsearchKibanaProxy.ts index fd6e45a21a..ae141fc5e8 100644 --- a/apps/desktop/src/lib/connection/elasticsearchKibanaProxy.ts +++ b/apps/desktop/src/lib/connection/elasticsearchKibanaProxy.ts @@ -5,6 +5,8 @@ export interface ElasticsearchExternalConfig { kibanaBasePath?: string; /** GET path for connect/test/health. Empty means GET /. */ connectivityCheckPath?: string; + /** When true, skip the connectivity check entirely (test/open never probes). */ + connectivityCheckDisabled?: boolean; /** Regex collapsing rolling/time-series index suffixes into a `*` pattern. Empty → default; "off" → no grouping. */ indexGroupingPattern?: string; } @@ -46,19 +48,28 @@ export function elasticsearchConnectivityCheckPathFromConfig(value: unknown): st return typeof path === "string" ? normalizeElasticsearchConnectivityCheckPath(path) : ""; } +export function elasticsearchConnectivityCheckDisabledFromConfig(value: unknown): boolean { + const disabled = externalConfigRecord(value).connectivityCheckDisabled; + if (disabled === true) return true; + if (typeof disabled !== "string") return false; + const normalized = disabled.trim().toLowerCase(); + return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on"; +} + export function elasticsearchIndexGroupingPatternFromConfig(value: unknown): string { const config = externalConfigRecord(value); const pattern = config.indexGroupingPattern; return typeof pattern === "string" ? pattern.trim() : ""; } -export function buildElasticsearchExternalConfig(mode: ElasticsearchConnectionMode, kibanaBasePath: string, connectivityCheckPath = "", indexGroupingPattern = ""): ElasticsearchExternalConfig | undefined { +export function buildElasticsearchExternalConfig(mode: ElasticsearchConnectionMode, kibanaBasePath: string, connectivityCheckPath = "", indexGroupingPattern = "", connectivityCheckDisabled = false): ElasticsearchExternalConfig | undefined { const checkPath = normalizeElasticsearchConnectivityCheckPath(connectivityCheckPath); const grouping = indexGroupingPattern.trim(); if (mode !== "kibana") { - if (!checkPath && !grouping) return undefined; + if (!checkPath && !grouping && !connectivityCheckDisabled) return undefined; const config: ElasticsearchExternalConfig = {}; if (checkPath) config.connectivityCheckPath = checkPath; + if (connectivityCheckDisabled) config.connectivityCheckDisabled = true; if (grouping) config.indexGroupingPattern = grouping; return config; } @@ -66,6 +77,7 @@ export function buildElasticsearchExternalConfig(mode: ElasticsearchConnectionMo const config: ElasticsearchExternalConfig = { mode: "kibana" }; if (normalizedPath) config.kibanaBasePath = normalizedPath; if (checkPath) config.connectivityCheckPath = checkPath; + if (connectivityCheckDisabled) config.connectivityCheckDisabled = true; if (grouping) config.indexGroupingPattern = grouping; return config; } diff --git a/crates/dbx-core/src/db/elasticsearch_driver.rs b/crates/dbx-core/src/db/elasticsearch_driver.rs index e3d3cbe9b0..cfb899a8f1 100644 --- a/crates/dbx-core/src/db/elasticsearch_driver.rs +++ b/crates/dbx-core/src/db/elasticsearch_driver.rs @@ -5,6 +5,7 @@ use serde::Deserialize; use serde_json::Value; use std::collections::{BTreeMap, HashMap, HashSet}; use std::error::Error; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::time::Duration; use super::{http_client_builder, with_connection_timeout}; @@ -51,6 +52,9 @@ pub struct EsClient { transport_mode: ElasticsearchTransportMode, /// GET path used for connect / health / test (default "/"). connectivity_check_path: String, + /// 为 true 时完全跳过连通性检查(test_connection 直接返回 Ok)。 + /// 用于账号连 `/` 或任何检查路径都无权限、且集群间权限不统一的场景。 + connectivity_check_disabled: bool, /// 正则:把易变的时间/滚动后缀折叠成 `*`,将同一前缀的滚动索引聚合成一个 /// pattern 节点。`None` 表示关闭聚合,展示原始索引名。 index_grouping: Option, @@ -72,6 +76,7 @@ impl EsClient { timeout, ElasticsearchTransportMode::Direct, "/".to_string(), + false, None, ) } @@ -84,6 +89,7 @@ impl EsClient { timeout: Duration, transport_mode: ElasticsearchTransportMode, connectivity_check_path: String, + connectivity_check_disabled: bool, index_grouping: Option, ) -> Self { let base_url = url.trim_end_matches('/').to_string(); @@ -91,10 +97,22 @@ impl EsClient { (Some(u), Some(p)) if !u.is_empty() => Some((u.to_string(), p.to_string())), _ => None, }; - let builder = http_client_builder(timeout).danger_accept_invalid_certs(accept_invalid_certs); + let mut builder = http_client_builder(timeout).danger_accept_invalid_certs(accept_invalid_certs); + if let Some(addrs) = elasticsearch_localhost_resolve_addrs(&base_url, connectivity_check_disabled) { + builder = builder.resolve_to_addrs("localhost", &addrs); + } let http = builder.build().unwrap_or_else(|_| HttpClient::new()); let fallback_base_urls = elasticsearch_base_url_fallbacks(&base_url); - Self { http, base_url, fallback_base_urls, auth, transport_mode, connectivity_check_path, index_grouping } + Self { + http, + base_url, + fallback_base_urls, + auth, + transport_mode, + connectivity_check_path, + connectivity_check_disabled, + index_grouping, + } } pub fn from_config( @@ -114,6 +132,7 @@ impl EsClient { }; let base_url = format!("{}{}", url.trim_end_matches('/'), kibana_base_path.as_deref().unwrap_or("")); let connectivity_check_path = elasticsearch_connectivity_check_path(external_config); + let connectivity_check_disabled = elasticsearch_connectivity_check_disabled(external_config); let index_grouping = elasticsearch_index_grouping(external_config); Self::new_with_mode( &base_url, @@ -123,6 +142,7 @@ impl EsClient { timeout, transport_mode, connectivity_check_path, + connectivity_check_disabled, index_grouping, ) } @@ -188,6 +208,7 @@ impl Clone for EsClient { auth: self.auth.clone(), transport_mode: self.transport_mode, connectivity_check_path: self.connectivity_check_path.clone(), + connectivity_check_disabled: self.connectivity_check_disabled, index_grouping: self.index_grouping.clone(), } } @@ -242,6 +263,23 @@ pub fn elasticsearch_connectivity_check_path(external_config: Option<&Value>) -> /// /// 语义:用 `${1}*` 模板替换匹配区间——正则带捕获组 1 时保留其内容做前缀, /// 不带捕获组时即把匹配到的“易变尾巴”替换成 `*`。 +/// 是否完全跳过连通性检查。读取配置 `connectivityCheckDisabled`(布尔)。 +/// 也兼容字符串 "true"/"1"/"yes"/"on"。用于账号无任何集群/索引探活权限的场景。 +pub fn elasticsearch_connectivity_check_disabled(external_config: Option<&Value>) -> bool { + let Some(value) = external_config.and_then(Value::as_object).and_then(|c| c.get("connectivityCheckDisabled")) + else { + return false; + }; + match value { + Value::Bool(b) => *b, + Value::String(s) => { + let s = s.trim(); + s.eq_ignore_ascii_case("true") || s == "1" || s.eq_ignore_ascii_case("yes") || s.eq_ignore_ascii_case("on") + } + _ => false, + } +} + pub fn elasticsearch_index_grouping(external_config: Option<&Value>) -> Option { let raw = external_config .and_then(Value::as_object) @@ -277,6 +315,10 @@ fn group_index_names(names: Vec, pattern: Option<&Regex>) -> Vec } pub async fn test_connection(client: &mut EsClient, timeout: Duration) -> Result<(), String> { + // 用户显式关闭连通性检查:不发探活请求,直接视为可连。 + if client.connectivity_check_disabled { + return Ok(()); + } let mut errors = Vec::new(); let urls = std::iter::once(client.base_url.clone()).chain(client.fallback_base_urls.clone()); let check_path = client.connectivity_check_path.clone(); @@ -356,6 +398,17 @@ fn elasticsearch_base_url_fallbacks(base_url: &str) -> Vec { } } +fn elasticsearch_localhost_resolve_addrs(base_url: &str, connectivity_check_disabled: bool) -> Option<[SocketAddr; 2]> { + if !connectivity_check_disabled { + return None; + } + let parsed = reqwest::Url::parse(base_url).ok()?; + if !parsed.host_str()?.eq_ignore_ascii_case("localhost") { + return None; + } + Some([SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0), SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)]) +} + fn elasticsearch_index_path(index: &str, endpoint: &str) -> String { format!("/{}/{}", elasticsearch_path_segment(index), endpoint.trim_start_matches('/')) } @@ -2273,6 +2326,18 @@ mod tests { assert_eq!(names, vec!["ngx-log-1".to_string(), "ngx-log-2".to_string()]); } + #[test] + fn connectivity_check_disabled_parses_bool_and_string() { + use super::elasticsearch_connectivity_check_disabled as disabled; + assert!(!disabled(None)); + assert!(!disabled(Some(&serde_json::json!({})))); + assert!(disabled(Some(&serde_json::json!({ "connectivityCheckDisabled": true })))); + assert!(disabled(Some(&serde_json::json!({ "connectivityCheckDisabled": "on" })))); + assert!(disabled(Some(&serde_json::json!({ "connectivityCheckDisabled": "TRUE" })))); + assert!(!disabled(Some(&serde_json::json!({ "connectivityCheckDisabled": false })))); + assert!(!disabled(Some(&serde_json::json!({ "connectivityCheckDisabled": "off" })))); + } + #[test] fn group_index_names_off_by_default() { // 缺省配置 → 关闭聚合,原样返回。 @@ -2406,6 +2471,51 @@ mod tests { assert_eq!(elasticsearch_base_url_fallbacks("https://search.example.com:9200"), Vec::::new()); } + #[test] + fn disabled_connectivity_check_keeps_localhost_request_fallback() { + assert_eq!( + super::elasticsearch_localhost_resolve_addrs("https://localhost:9200", true), + Some(["[::1]:0".parse().unwrap(), "127.0.0.1:0".parse().unwrap()]) + ); + assert_eq!(super::elasticsearch_localhost_resolve_addrs("https://localhost:9200", false), None); + assert_eq!(super::elasticsearch_localhost_resolve_addrs("https://search.example.com:9200", true), None); + } + + #[tokio::test] + async fn disabled_connectivity_check_can_request_ipv4_localhost() { + use tokio::io::AsyncWriteExt; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let server = tokio::spawn(async move { + let (mut socket, _) = listener.accept().await.unwrap(); + let request = read_http_request(&mut socket).await; + assert!(request.starts_with("GET /_cluster/health "), "unexpected request: {request}"); + let body = r#"{"status":"green"}"#; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + socket.write_all(response.as_bytes()).await.unwrap(); + }); + + let mut client = EsClient::from_config( + &format!("http://localhost:{}", addr.port()), + None, + None, + false, + None, + Some(&json!({ "connectivityCheckDisabled": "yes" })), + Duration::from_secs(2), + ); + super::test_connection(&mut client, Duration::from_secs(2)).await.unwrap(); + let response = client.get("/_cluster/health").send().await.unwrap(); + + assert!(response.status().is_success()); + server.await.unwrap(); + } + #[test] fn elasticsearch_client_from_config_uses_url_params_for_tls_verification() { let client = EsClient::from_config(