From 8b8d4edfa43c2c6a0c0238b7aed0c4af1abe57cf Mon Sep 17 00:00:00 2001 From: Ken Roland Date: Mon, 17 Mar 2025 10:55:24 -0400 Subject: [PATCH 1/4] Add our remote info back Signed-off-by: rtkkroland --- VERSION | 2 +- collector/remote_info.go | 188 ++++++++++++++++++++++++++++++ collector/remote_info_response.go | 28 +++++ collector/remote_info_test.go | 56 +++++++++ main.go | 7 ++ 5 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 collector/remote_info.go create mode 100644 collector/remote_info_response.go create mode 100644 collector/remote_info_test.go diff --git a/VERSION b/VERSION index f8e233b2..c8e38b61 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.0 +2.9.0 diff --git a/collector/remote_info.go b/collector/remote_info.go new file mode 100644 index 00000000..eca15deb --- /dev/null +++ b/collector/remote_info.go @@ -0,0 +1,188 @@ +// Copyright 2021 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "encoding/json" + "fmt" + "log/slog" + "net/http" + "net/url" + "path" + + "github.com/prometheus/client_golang/prometheus" +) + +// Labels for remote info metrics +var defaulRemoteInfoLabels = []string{"remote_cluster"} +var defaultRemoteInfoLabelValues = func(remote_cluster string) []string { + return []string{ + remote_cluster, + } +} + +type remoteInfoMetric struct { + Type prometheus.ValueType + Desc *prometheus.Desc + Value func(remoteStats RemoteCluster) float64 + Labels func(remote_cluster string) []string +} + +// RemoteInfo information struct +type RemoteInfo struct { + logger *slog.Logger + client *http.Client + url *url.URL + + up prometheus.Gauge + totalScrapes, jsonParseFailures prometheus.Counter + + remoteInfoMetrics []*remoteInfoMetric +} + +// NewClusterSettings defines Cluster Settings Prometheus metrics +func NewRemoteInfo(logger *slog.Logger, client *http.Client, url *url.URL) *RemoteInfo { + + return &RemoteInfo{ + logger: logger, + client: client, + url: url, + + up: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "remote_info_stats", "up"), + Help: "Was the last scrape of the ElasticSearch remote info endpoint successful.", + }), + totalScrapes: prometheus.NewCounter(prometheus.CounterOpts{ + Name: prometheus.BuildFQName(namespace, "remote_info_stats", "total_scrapes"), + Help: "Current total ElasticSearch remote info scrapes.", + }), + jsonParseFailures: prometheus.NewCounter(prometheus.CounterOpts{ + Name: prometheus.BuildFQName(namespace, "remote_info_stats", "json_parse_failures"), + Help: "Number of errors while parsing JSON.", + }), + // Send all of the remote metrics + remoteInfoMetrics: []*remoteInfoMetric{ + { + Type: prometheus.GaugeValue, + Desc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "remote_info", "num_nodes_connected"), + "Number of nodes connected", defaulRemoteInfoLabels, nil, + ), + Value: func(remoteStats RemoteCluster) float64 { + return float64(remoteStats.NumNodesConnected) + }, + Labels: defaultRemoteInfoLabelValues, + }, + { + Type: prometheus.GaugeValue, + Desc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "remote_info", "num_proxy_sockets_connected"), + "Number of proxy sockets connected", defaulRemoteInfoLabels, nil, + ), + Value: func(remoteStats RemoteCluster) float64 { + return float64(remoteStats.NumProxySocketsConnected) + }, + Labels: defaultRemoteInfoLabelValues, + }, + { + Type: prometheus.GaugeValue, + Desc: prometheus.NewDesc( + prometheus.BuildFQName(namespace, "remote_info", "max_connections_per_cluster"), + "Max connections per cluster", defaulRemoteInfoLabels, nil, + ), + Value: func(remoteStats RemoteCluster) float64 { + return float64(remoteStats.MaxConnectionsPerCluster) + }, + Labels: defaultRemoteInfoLabelValues, + }, + }, + } +} + +func (c *RemoteInfo) fetchAndDecodeRemoteInfoStats() (RemoteInfoResponse, error) { + var rir RemoteInfoResponse + + u := *c.url + u.Path = path.Join(u.Path, "/_remote/info") + + res, err := c.client.Get(u.String()) + if err != nil { + return rir, fmt.Errorf("failed to get remote info from %s://%s:%s%s: %s", + u.Scheme, u.Hostname(), u.Port(), u.Path, err) + } + + defer func() { + err = res.Body.Close() + if err != nil { + c.logger.Warn( + "failed to close http.Client", + "err", err, + ) + } + }() + + if res.StatusCode != http.StatusOK { + return rir, fmt.Errorf("HTTP Request failed with code %d", res.StatusCode) + } + + if err := json.NewDecoder(res.Body).Decode(&rir); err != nil { + c.jsonParseFailures.Inc() + return rir, err + } + return rir, nil +} + +// Collect gets remote info values +func (ri *RemoteInfo) Collect(ch chan<- prometheus.Metric) { + ri.totalScrapes.Inc() + defer func() { + ch <- ri.up + ch <- ri.totalScrapes + ch <- ri.jsonParseFailures + }() + + remoteInfoResp, err := ri.fetchAndDecodeRemoteInfoStats() + if err != nil { + ri.up.Set(0) + ri.logger.Warn( + "failed to fetch and decode remote info", + "err", err, + ) + return + } + ri.totalScrapes.Inc() + ri.up.Set(1) + + // Remote Info + for remote_cluster, remoteInfo := range remoteInfoResp { + for _, metric := range ri.remoteInfoMetrics { + ch <- prometheus.MustNewConstMetric( + metric.Desc, + metric.Type, + metric.Value(remoteInfo), + metric.Labels(remote_cluster)..., + ) + } + } +} + +// Describe add Indices metrics descriptions +func (ri *RemoteInfo) Describe(ch chan<- *prometheus.Desc) { + for _, metric := range ri.remoteInfoMetrics { + ch <- metric.Desc + } + ch <- ri.up.Desc() + ch <- ri.totalScrapes.Desc() + ch <- ri.jsonParseFailures.Desc() +} diff --git a/collector/remote_info_response.go b/collector/remote_info_response.go new file mode 100644 index 00000000..ecd3e106 --- /dev/null +++ b/collector/remote_info_response.go @@ -0,0 +1,28 @@ +// Copyright 2021 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +// RemoteInfoResponse is a representation of a Elasticsearch _remote/info +type RemoteInfoResponse map[string]RemoteCluster + +// RemoteClsuter defines the struct of the tree for the Remote Cluster +type RemoteCluster struct { + Seeds []string `json:"seeds"` + Connected bool `json:"connected"` + NumNodesConnected int64 `json:"num_nodes_connected"` + NumProxySocketsConnected int64 `json:"num_proxy_sockets_connected"` + MaxConnectionsPerCluster int64 `json:"max_connections_per_cluster"` + InitialConnectTimeout string `json:"initial_connect_timeout"` + SkipUnavailable bool `json:"skip_unavailable"` +} diff --git a/collector/remote_info_test.go b/collector/remote_info_test.go new file mode 100644 index 00000000..1e9159a0 --- /dev/null +++ b/collector/remote_info_test.go @@ -0,0 +1,56 @@ +// Copyright 2021 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package collector + +import ( + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/prometheus/common/promslog" +) + +func TestRemoteInfoStats(t *testing.T) { + // Testcases created using: + // docker run -d -p 9200:9200 elasticsearch:VERSION-alpine + // curl http://localhost:9200/_cluster/settings/?include_defaults=true + files := []string{"../fixtures/settings-5.4.2.json", "../fixtures/settings-merge-5.4.2.json"} + for _, filename := range files { + f, _ := os.Open(filename) + defer f.Close() + for hn, handler := range map[string]http.Handler{ + "plain": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.Copy(w, f) + }), + } { + ts := httptest.NewServer(handler) + defer ts.Close() + + u, err := url.Parse(ts.URL) + if err != nil { + t.Fatalf("Failed to parse URL: %s", err) + } + c := NewRemoteInfo(promslog.NewNopLogger(), http.DefaultClient, u) + nsr, err := c.fetchAndDecodeRemoteInfoStats() + if err != nil { + t.Fatalf("Failed to fetch or decode remote info stats: %s", err) + } + t.Logf("[%s/%s] Remote Info Stats Response: %+v", hn, filename, nsr) + + } + } +} diff --git a/main.go b/main.go index c739c28d..19f94212 100644 --- a/main.go +++ b/main.go @@ -101,6 +101,9 @@ func main() { esInsecureSkipVerify = kingpin.Flag("es.ssl-skip-verify", "Skip SSL verification when connecting to Elasticsearch."). Default("false").Bool() + esExportRemoteInfo = kingpin.Flag("es.remote_info", + "Export stats associated with configured remote clusters."). + Default("false").Envar("ES_REMOTE_INFO").Bool() logOutput = kingpin.Flag("log.output", "Sets the log output. Valid outputs are stdout and stderr"). Default("stdout").String() @@ -240,6 +243,10 @@ func main() { prometheus.MustRegister(collector.NewIndicesMappings(logger, httpClient, esURL)) } + if *esExportRemoteInfo { + prometheus.MustRegister(collector.NewRemoteInfo(logger, httpClient, esURL)) + } + // start the cluster info retriever switch runErr := clusterInfoRetriever.Run(ctx); runErr { case nil: From e41493fbe5689856ed06278076dae9356fe48f3c Mon Sep 17 00:00:00 2001 From: rtkkroland Date: Fri, 19 Sep 2025 17:30:58 +0000 Subject: [PATCH 2/4] Add unit tests to remote info Signed-off-by: rtkkroland --- collector/remote_info.go | 1 - collector/remote_info_test.go | 173 ++++++++++++++++++++++++++++--- fixtures/remote_info/7.15.0.json | 20 ++++ fixtures/remote_info/8.0.0.json | 11 ++ fixtures/remote_info/empty.json | 1 + 5 files changed, 189 insertions(+), 17 deletions(-) create mode 100644 fixtures/remote_info/7.15.0.json create mode 100644 fixtures/remote_info/8.0.0.json create mode 100644 fixtures/remote_info/empty.json diff --git a/collector/remote_info.go b/collector/remote_info.go index eca15deb..44af0ba7 100644 --- a/collector/remote_info.go +++ b/collector/remote_info.go @@ -161,7 +161,6 @@ func (ri *RemoteInfo) Collect(ch chan<- prometheus.Metric) { ) return } - ri.totalScrapes.Inc() ri.up.Set(1) // Remote Info diff --git a/collector/remote_info_test.go b/collector/remote_info_test.go index 1e9159a0..8940877c 100644 --- a/collector/remote_info_test.go +++ b/collector/remote_info_test.go @@ -19,38 +19,179 @@ import ( "net/http/httptest" "net/url" "os" + "strings" "testing" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/prometheus/common/promslog" ) -func TestRemoteInfoStats(t *testing.T) { +func TestRemoteInfo(t *testing.T) { // Testcases created using: // docker run -d -p 9200:9200 elasticsearch:VERSION-alpine - // curl http://localhost:9200/_cluster/settings/?include_defaults=true - files := []string{"../fixtures/settings-5.4.2.json", "../fixtures/settings-merge-5.4.2.json"} - for _, filename := range files { - f, _ := os.Open(filename) - defer f.Close() - for hn, handler := range map[string]http.Handler{ - "plain": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // curl http://localhost:9200/_remote/info + + tests := []struct { + name string + file string + want string + }{ + { + name: "7.15.0", + file: "../fixtures/remote_info/7.15.0.json", + want: ` + # HELP elasticsearch_remote_info_max_connections_per_cluster Max connections per cluster + # TYPE elasticsearch_remote_info_max_connections_per_cluster gauge + elasticsearch_remote_info_max_connections_per_cluster{remote_cluster="cluster_remote_1"} 10 + elasticsearch_remote_info_max_connections_per_cluster{remote_cluster="cluster_remote_2"} 5 + # HELP elasticsearch_remote_info_num_nodes_connected Number of nodes connected + # TYPE elasticsearch_remote_info_num_nodes_connected gauge + elasticsearch_remote_info_num_nodes_connected{remote_cluster="cluster_remote_1"} 3 + elasticsearch_remote_info_num_nodes_connected{remote_cluster="cluster_remote_2"} 0 + # HELP elasticsearch_remote_info_num_proxy_sockets_connected Number of proxy sockets connected + # TYPE elasticsearch_remote_info_num_proxy_sockets_connected gauge + elasticsearch_remote_info_num_proxy_sockets_connected{remote_cluster="cluster_remote_1"} 5 + elasticsearch_remote_info_num_proxy_sockets_connected{remote_cluster="cluster_remote_2"} 0 + # HELP elasticsearch_remote_info_stats_json_parse_failures Number of errors while parsing JSON. + # TYPE elasticsearch_remote_info_stats_json_parse_failures counter + elasticsearch_remote_info_stats_json_parse_failures 0 + # HELP elasticsearch_remote_info_stats_total_scrapes Current total ElasticSearch remote info scrapes. + # TYPE elasticsearch_remote_info_stats_total_scrapes counter + elasticsearch_remote_info_stats_total_scrapes 1 + # HELP elasticsearch_remote_info_stats_up Was the last scrape of the ElasticSearch remote info endpoint successful. + # TYPE elasticsearch_remote_info_stats_up gauge + elasticsearch_remote_info_stats_up 1 + `, + }, + { + name: "8.0.0", + file: "../fixtures/remote_info/8.0.0.json", + want: ` + # HELP elasticsearch_remote_info_max_connections_per_cluster Max connections per cluster + # TYPE elasticsearch_remote_info_max_connections_per_cluster gauge + elasticsearch_remote_info_max_connections_per_cluster{remote_cluster="prod_cluster"} 30 + # HELP elasticsearch_remote_info_num_nodes_connected Number of nodes connected + # TYPE elasticsearch_remote_info_num_nodes_connected gauge + elasticsearch_remote_info_num_nodes_connected{remote_cluster="prod_cluster"} 15 + # HELP elasticsearch_remote_info_num_proxy_sockets_connected Number of proxy sockets connected + # TYPE elasticsearch_remote_info_num_proxy_sockets_connected gauge + elasticsearch_remote_info_num_proxy_sockets_connected{remote_cluster="prod_cluster"} 25 + # HELP elasticsearch_remote_info_stats_json_parse_failures Number of errors while parsing JSON. + # TYPE elasticsearch_remote_info_stats_json_parse_failures counter + elasticsearch_remote_info_stats_json_parse_failures 0 + # HELP elasticsearch_remote_info_stats_total_scrapes Current total ElasticSearch remote info scrapes. + # TYPE elasticsearch_remote_info_stats_total_scrapes counter + elasticsearch_remote_info_stats_total_scrapes 1 + # HELP elasticsearch_remote_info_stats_up Was the last scrape of the ElasticSearch remote info endpoint successful. + # TYPE elasticsearch_remote_info_stats_up gauge + elasticsearch_remote_info_stats_up 1 + `, + }, + { + name: "empty", + file: "../fixtures/remote_info/empty.json", + want: ` + # HELP elasticsearch_remote_info_stats_json_parse_failures Number of errors while parsing JSON. + # TYPE elasticsearch_remote_info_stats_json_parse_failures counter + elasticsearch_remote_info_stats_json_parse_failures 0 + # HELP elasticsearch_remote_info_stats_total_scrapes Current total ElasticSearch remote info scrapes. + # TYPE elasticsearch_remote_info_stats_total_scrapes counter + elasticsearch_remote_info_stats_total_scrapes 1 + # HELP elasticsearch_remote_info_stats_up Was the last scrape of the ElasticSearch remote info endpoint successful. + # TYPE elasticsearch_remote_info_stats_up gauge + elasticsearch_remote_info_stats_up 1 + `, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.Copy(w, f) - }), - } { - ts := httptest.NewServer(handler) + })) defer ts.Close() u, err := url.Parse(ts.URL) if err != nil { - t.Fatalf("Failed to parse URL: %s", err) + t.Fatal(err) } + c := NewRemoteInfo(promslog.NewNopLogger(), http.DefaultClient, u) - nsr, err := c.fetchAndDecodeRemoteInfoStats() if err != nil { - t.Fatalf("Failed to fetch or decode remote info stats: %s", err) + t.Fatal(err) + } + + if err := testutil.CollectAndCompare(c, strings.NewReader(tt.want)); err != nil { + t.Fatal(err) } - t.Logf("[%s/%s] Remote Info Stats Response: %+v", hn, filename, nsr) + }) + } +} + +func TestRemoteInfoError(t *testing.T) { + // Test error handling when endpoint is unavailable + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + })) + defer ts.Close() + + u, err := url.Parse(ts.URL) + if err != nil { + t.Fatal(err) + } + + c := NewRemoteInfo(promslog.NewNopLogger(), http.DefaultClient, u) + + expected := ` + # HELP elasticsearch_remote_info_stats_json_parse_failures Number of errors while parsing JSON. + # TYPE elasticsearch_remote_info_stats_json_parse_failures counter + elasticsearch_remote_info_stats_json_parse_failures 0 + # HELP elasticsearch_remote_info_stats_total_scrapes Current total ElasticSearch remote info scrapes. + # TYPE elasticsearch_remote_info_stats_total_scrapes counter + elasticsearch_remote_info_stats_total_scrapes 1 + # HELP elasticsearch_remote_info_stats_up Was the last scrape of the ElasticSearch remote info endpoint successful. + # TYPE elasticsearch_remote_info_stats_up gauge + elasticsearch_remote_info_stats_up 0 + ` + + if err := testutil.CollectAndCompare(c, strings.NewReader(expected)); err != nil { + t.Fatal(err) + } +} + +func TestRemoteInfoJSONParseError(t *testing.T) { + // Test JSON parse error handling + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("invalid json")) + })) + defer ts.Close() + + u, err := url.Parse(ts.URL) + if err != nil { + t.Fatal(err) + } + + c := NewRemoteInfo(promslog.NewNopLogger(), http.DefaultClient, u) + + expected := ` + # HELP elasticsearch_remote_info_stats_json_parse_failures Number of errors while parsing JSON. + # TYPE elasticsearch_remote_info_stats_json_parse_failures counter + elasticsearch_remote_info_stats_json_parse_failures 1 + # HELP elasticsearch_remote_info_stats_total_scrapes Current total ElasticSearch remote info scrapes. + # TYPE elasticsearch_remote_info_stats_total_scrapes counter + elasticsearch_remote_info_stats_total_scrapes 1 + # HELP elasticsearch_remote_info_stats_up Was the last scrape of the ElasticSearch remote info endpoint successful. + # TYPE elasticsearch_remote_info_stats_up gauge + elasticsearch_remote_info_stats_up 0 + ` - } + if err := testutil.CollectAndCompare(c, strings.NewReader(expected)); err != nil { + t.Fatal(err) } } diff --git a/fixtures/remote_info/7.15.0.json b/fixtures/remote_info/7.15.0.json new file mode 100644 index 00000000..deb67cc2 --- /dev/null +++ b/fixtures/remote_info/7.15.0.json @@ -0,0 +1,20 @@ +{ + "cluster_remote_1": { + "seeds": ["192.168.1.100:9300", "192.168.1.101:9300"], + "connected": true, + "num_nodes_connected": 3, + "num_proxy_sockets_connected": 5, + "max_connections_per_cluster": 10, + "initial_connect_timeout": "30s", + "skip_unavailable": false + }, + "cluster_remote_2": { + "seeds": ["10.0.0.50:9300"], + "connected": false, + "num_nodes_connected": 0, + "num_proxy_sockets_connected": 0, + "max_connections_per_cluster": 5, + "initial_connect_timeout": "30s", + "skip_unavailable": true + } +} \ No newline at end of file diff --git a/fixtures/remote_info/8.0.0.json b/fixtures/remote_info/8.0.0.json new file mode 100644 index 00000000..2ab1705b --- /dev/null +++ b/fixtures/remote_info/8.0.0.json @@ -0,0 +1,11 @@ +{ + "prod_cluster": { + "seeds": ["prod-es-1:9300", "prod-es-2:9300", "prod-es-3:9300"], + "connected": true, + "num_nodes_connected": 15, + "num_proxy_sockets_connected": 25, + "max_connections_per_cluster": 30, + "initial_connect_timeout": "60s", + "skip_unavailable": false + } +} \ No newline at end of file diff --git a/fixtures/remote_info/empty.json b/fixtures/remote_info/empty.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/fixtures/remote_info/empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file From 339d71ccd5da5e23c93061e8c974fb48f0d8524e Mon Sep 17 00:00:00 2001 From: rtkkroland Date: Mon, 3 Nov 2025 11:45:54 -0500 Subject: [PATCH 3/4] Update docs Signed-off-by: rtkkroland --- README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/README.md b/README.md index 5099b273..20861754 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ elasticsearch_exporter --help | collector.health-report | 1.10.0 | If true, query the health report (requires elasticsearch 8.7.0 or later) | false | | es.slm | | If true, query stats for SLM. | false | | es.data_stream | | If true, query state for Data Steams. | false | +| es.remote_info | 2.x.x | If true, query stats for configured remote clusters in the Elasticsearch cluster. Exposes connection metrics for cross-cluster search and replication. | false | | es.timeout | 1.0.2 | Timeout for trying to get stats from Elasticsearch. (ex: 20s) | 5s | | es.ca | 1.0.2 | Path to PEM file that contains trusted Certificate Authorities for the Elasticsearch connection. | | | es.client-private-key | 1.0.2 | Path to PEM file that contains the private key for client auth when connecting to Elasticsearch. | | @@ -107,6 +108,7 @@ es.shards | not sure if `indices` or `cluster` `monitor` or both | collector.snapshots | `cluster:admin/snapshot/status` and `cluster:admin/repository/get` | [ES Forum Post](https://discuss.elastic.co/t/permissions-for-backup-user-with-x-pack/88057) es.slm | `manage_slm` es.data_stream | `monitor` or `manage` (per index or `*`) | +es.remote_info | `cluster` `monitor` | Required for accessing remote cluster connection information via the `/_remote/info` endpoint Further Information @@ -175,6 +177,49 @@ Notes: - Any `options:` under an auth module will be appended as URL query parameters to the target URL. - The `tls` auth module (client certificate authentication) is intended for self‑managed Elasticsearch/OpenSearch deployments. Amazon OpenSearch Service typically authenticates at the domain edge with IAM/SigV4 and does not support client certificate authentication; use the `aws` auth module instead when scraping Amazon OpenSearch Service domains. +### Remote Cluster Monitoring + +The remote info collector (`es.remote_info`) provides monitoring capabilities for Elasticsearch cross-cluster search and cross-cluster replication configurations. This collector queries the `/_remote/info` endpoint to gather connection statistics for configured remote clusters. + +#### When to Enable + +Enable this collector when you have: +- Cross-cluster search configured +- Cross-cluster replication set up +- Multiple Elasticsearch clusters connected via remote cluster connections +- Need to monitor the health and connectivity of remote cluster connections + +#### Metrics Provided + +The collector provides connection metrics labeled by `remote_cluster` name, including: +- Active node connections to remote clusters +- Proxy socket connections (for clusters behind proxies) +- Maximum connection limits per cluster +- Connection health and scrape statistics + +#### Prerequisites + +- Remote clusters must be properly configured in your Elasticsearch cluster +- The user account must have `cluster:monitor` privileges to access the `/_remote/info` endpoint +- Remote clusters should be accessible and properly configured with seeds + +#### Example Configuration + +To enable remote cluster monitoring: + +```bash +./elasticsearch_exporter --es.uri=http://localhost:9200 --es.remote_info +``` + +The collector will automatically discover all configured remote clusters and expose metrics for each one. + +The remote info collector can also be enabled via the `ES_REMOTE_INFO` environment variable: + +```bash +export ES_REMOTE_INFO=true +./elasticsearch_exporter --es.uri=http://localhost:9200 +``` + ### Metrics See the [metrics documentation](metrics.md) From e6b1e02ba55322c5723a5d41eae42945b7fcb530 Mon Sep 17 00:00:00 2001 From: rtkkroland Date: Mon, 24 Nov 2025 10:01:03 -0500 Subject: [PATCH 4/4] revert version change Signed-off-by: rtkkroland --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index c8e38b61..f8e233b2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.9.0 +1.9.0