diff --git a/docs/reference/searchable-snapshots/apis/clear-cache.asciidoc b/docs/reference/searchable-snapshots/apis/clear-cache.asciidoc index 9d5685fdc68..5ca9cc5a4e0 100644 --- a/docs/reference/searchable-snapshots/apis/clear-cache.asciidoc +++ b/docs/reference/searchable-snapshots/apis/clear-cache.asciidoc @@ -1,5 +1,5 @@ [role="xpack"] -[testenv="basic"] +[testenv="platinum"] [[searchable-snapshots-api-clear-cache]] === Clear cache API ++++ diff --git a/docs/reference/searchable-snapshots/apis/get-stats.asciidoc b/docs/reference/searchable-snapshots/apis/get-stats.asciidoc index c54fe96d9f8..4a8914553a5 100644 --- a/docs/reference/searchable-snapshots/apis/get-stats.asciidoc +++ b/docs/reference/searchable-snapshots/apis/get-stats.asciidoc @@ -1,5 +1,5 @@ [role="xpack"] -[testenv="basic"] +[testenv="platinum"] [[searchable-snapshots-api-stats]] === Searchable snapshot statistics API ++++ diff --git a/docs/reference/searchable-snapshots/apis/mount-snapshot.asciidoc b/docs/reference/searchable-snapshots/apis/mount-snapshot.asciidoc index 7522e64944e..04bd9d5ad80 100644 --- a/docs/reference/searchable-snapshots/apis/mount-snapshot.asciidoc +++ b/docs/reference/searchable-snapshots/apis/mount-snapshot.asciidoc @@ -1,5 +1,5 @@ [role="xpack"] -[testenv="basic"] +[testenv="platinum"] [[searchable-snapshots-api-mount-snapshot]] === Mount snapshot API ++++ diff --git a/docs/reference/searchable-snapshots/apis/repository-stats.asciidoc b/docs/reference/searchable-snapshots/apis/repository-stats.asciidoc new file mode 100644 index 00000000000..2818b13fa52 --- /dev/null +++ b/docs/reference/searchable-snapshots/apis/repository-stats.asciidoc @@ -0,0 +1,73 @@ +[role="xpack"] +[testenv="platinum"] +[[searchable-snapshots-repository-stats]] +=== Searchable snapshot repository statistics API +++++ +Searchable snapshot repository statistics +++++ + +experimental[] + +Retrieve usage statistics about a snapshot repository. + +[[searchable-snapshots-repository-stats-request]] +==== {api-request-title} + +`GET /_snapshot//_stats` + +[[searchable-snapshots-repository-stats-prereqs]] +==== {api-prereq-title} + +If the {es} {security-features} are enabled, you must have the +`manage` cluster privilege and the `manage` index privilege +for any included indices to use this API. +For more information, see <>. + +[[searchable-snapshots-repository-stats-desc]] +==== {api-description-title} + + +[[searchable-snapshots-repository-stats-path-params]] +==== {api-path-parms-title} + +``:: +(Required, string) +The repository for which to retrieve stats. + + +[[searchable-snapshots-repository-stats-example]] +==== {api-examples-title} +//// +[source,console] +----------------------------------- +PUT /docs +{ + "settings" : { + "index.number_of_shards" : 1, + "index.number_of_replicas" : 0 + } +} + +PUT /_snapshot/my_repository/my_snapshot?wait_for_completion=true +{ + "include_global_state": false, + "indices": "docs" +} + +DELETE /docs + +POST /_snapshot/my_repository/my_snapshot/_mount?wait_for_completion=true +{ + "index": "docs" +} +----------------------------------- +// TEST[setup:setup-repository] +//// + +Retrieves the statistics of the repository `my_repository`: + +[source,console] +-------------------------------------------------- +GET /_snapshot/my_repository/_stats +-------------------------------------------------- +// TEST[continued] diff --git a/docs/reference/searchable-snapshots/apis/searchable-snapshots-apis.asciidoc b/docs/reference/searchable-snapshots/apis/searchable-snapshots-apis.asciidoc index 5b56c644090..6c4f84f70e4 100644 --- a/docs/reference/searchable-snapshots/apis/searchable-snapshots-apis.asciidoc +++ b/docs/reference/searchable-snapshots/apis/searchable-snapshots-apis.asciidoc @@ -1,5 +1,5 @@ [role="xpack"] -[testenv="basic"] +[testenv="platinum"] [[searchable-snapshots-apis]] == Searchable snapshots APIs @@ -10,7 +10,9 @@ You can use the following APIs to perform searchable snapshots operations. * <> * <> * <> +* <> include::mount-snapshot.asciidoc[] include::clear-cache.asciidoc[] include::get-stats.asciidoc[] +include::repository-stats.asciidoc[] diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java index 1e82c7c592b..177512784b3 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java @@ -151,6 +151,7 @@ class S3BlobContainer extends AbstractBlobContainer { final ListObjectsRequest listObjectsRequest = new ListObjectsRequest(); listObjectsRequest.setBucketName(blobStore.bucket()); listObjectsRequest.setPrefix(keyPath); + listObjectsRequest.setRequestMetricCollector(blobStore.listMetricCollector); list = SocketAccess.doPrivileged(() -> clientReference.client().listObjects(listObjectsRequest)); } final List blobsToDelete = new ArrayList<>(); @@ -307,7 +308,8 @@ class S3BlobContainer extends AbstractBlobContainer { } private ListObjectsRequest listObjectsRequest(String keyPath) { - return new ListObjectsRequest().withBucketName(blobStore.bucket()).withPrefix(keyPath).withDelimiter("/"); + return new ListObjectsRequest().withBucketName(blobStore.bucket()).withPrefix(keyPath).withDelimiter("/") + .withRequestMetricCollector(blobStore.listMetricCollector); } private String buildKey(String blobName) { diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java index 101925d96d8..cec264efe8e 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobStore.java @@ -19,8 +19,12 @@ package org.elasticsearch.repositories.s3; +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.metrics.RequestMetricCollector; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.StorageClass; +import com.amazonaws.util.AWSRequestMetrics; import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; @@ -29,7 +33,10 @@ import org.elasticsearch.common.blobstore.BlobStoreException; import org.elasticsearch.common.unit.ByteSizeValue; import java.io.IOException; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; class S3BlobStore implements BlobStore { @@ -47,6 +54,12 @@ class S3BlobStore implements BlobStore { private final RepositoryMetadata repositoryMetadata; + private final Stats stats = new Stats(); + + final RequestMetricCollector getMetricCollector; + final RequestMetricCollector listMetricCollector; + + S3BlobStore(S3Service service, String bucket, boolean serverSideEncryption, ByteSizeValue bufferSize, String cannedACL, String storageClass, RepositoryMetadata repositoryMetadata) { @@ -57,6 +70,26 @@ class S3BlobStore implements BlobStore { this.cannedACL = initCannedACL(cannedACL); this.storageClass = initStorageClass(storageClass); this.repositoryMetadata = repositoryMetadata; + this.getMetricCollector = new RequestMetricCollector() { + @Override + public void collectMetrics(Request request, Response response) { + assert request.getHttpMethod().name().equals("GET"); + final Number requestCount = request.getAWSRequestMetrics().getTimingInfo() + .getCounter(AWSRequestMetrics.Field.RequestCount.name()); + assert requestCount != null; + stats.getCount.addAndGet(requestCount.longValue()); + } + }; + this.listMetricCollector = new RequestMetricCollector() { + @Override + public void collectMetrics(Request request, Response response) { + assert request.getHttpMethod().name().equals("GET"); + final Number requestCount = request.getAWSRequestMetrics().getTimingInfo() + .getCounter(AWSRequestMetrics.Field.RequestCount.name()); + assert requestCount != null; + stats.listCount.addAndGet(requestCount.longValue()); + } + }; } @Override @@ -94,6 +127,11 @@ class S3BlobStore implements BlobStore { this.service.close(); } + @Override + public Map stats() { + return stats.toMap(); + } + public CannedAccessControlList getCannedACL() { return cannedACL; } @@ -135,4 +173,18 @@ class S3BlobStore implements BlobStore { throw new BlobStoreException("cannedACL is not valid: [" + cannedACL + "]"); } + + static class Stats { + + final AtomicLong listCount = new AtomicLong(); + + final AtomicLong getCount = new AtomicLong(); + + Map toMap() { + final Map results = new HashMap<>(); + results.put("GET", getCount.get()); + results.put("LIST", listCount.get()); + return results; + } + } } diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java index 105f469c905..4462bd51e77 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3RetryingInputStream.java @@ -25,9 +25,9 @@ import com.amazonaws.services.s3.model.S3Object; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.Version; import org.elasticsearch.common.io.Streams; import org.elasticsearch.core.internal.io.IOUtils; -import org.elasticsearch.Version; import java.io.IOException; import java.io.InputStream; @@ -83,6 +83,7 @@ class S3RetryingInputStream extends InputStream { private InputStream openStream() throws IOException { try (AmazonS3Reference clientReference = blobStore.clientReference()) { final GetObjectRequest getObjectRequest = new GetObjectRequest(blobStore.bucket(), blobKey); + getObjectRequest.setRequestMetricCollector(blobStore.getMetricCollector); if (currentOffset > 0 || start > 0 || end < Long.MAX_VALUE - 1) { assert start + currentOffset <= end : "requesting beyond end, start = " + start + " offset=" + currentOffset + " end=" + end; diff --git a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java index ab04fd2d453..e3fab59e7d4 100644 --- a/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java +++ b/plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java @@ -23,7 +23,9 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import fixture.s3.S3HttpHandler; import org.elasticsearch.action.ActionRunnable; +import org.elasticsearch.action.admin.indices.forcemerge.ForceMergeResponse; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.metadata.IndexMetadata; import org.elasticsearch.cluster.metadata.RepositoryMetadata; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.SuppressForbidden; @@ -31,6 +33,7 @@ import org.elasticsearch.common.blobstore.BlobContainer; import org.elasticsearch.common.blobstore.BlobPath; import org.elasticsearch.common.blobstore.BlobStore; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; @@ -41,11 +44,14 @@ import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.repositories.RepositoryData; +import org.elasticsearch.repositories.RepositoryMissingException; +import org.elasticsearch.repositories.RepositoryStats; import org.elasticsearch.repositories.blobstore.BlobStoreRepository; import org.elasticsearch.repositories.blobstore.ESMockAPIBasedRepositoryIntegTestCase; import org.elasticsearch.snapshots.SnapshotId; import org.elasticsearch.snapshots.SnapshotsService; import org.elasticsearch.snapshots.mockstore.BlobStoreWrapper; +import org.elasticsearch.test.BackgroundIndexer; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.threadpool.ThreadPool; @@ -56,8 +62,13 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.StreamSupport; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; +import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.startsWith; @@ -108,12 +119,12 @@ public class S3BlobStoreRepositoryTests extends ESMockAPIBasedRepositoryIntegTes @Override protected Map createHttpHandlers() { - return Collections.singletonMap("/bucket", new S3BlobStoreHttpHandler("bucket")); + return Collections.singletonMap("/bucket", new S3StatsHttpHandler(new S3BlobStoreHttpHandler("bucket"))); } @Override protected HttpHandler createErroneousHttpHandler(final HttpHandler delegate) { - return new S3ErroneousHttpHandler(delegate, randomIntBetween(2, 3)); + return new S3StatsHttpHandler(new S3ErroneousHttpHandler(delegate, randomIntBetween(2, 3))); } @Override @@ -176,6 +187,81 @@ public class S3BlobStoreRepositoryTests extends ESMockAPIBasedRepositoryIntegTes assertThat(repository.threadPool().relativeTimeInNanos() - beforeFastDelete, lessThan(TEST_COOLDOWN_PERIOD.getNanos())); } + public void testRequestStats() throws Exception { + final String repository = createRepository(randomName()); + final String index = "index-no-merges"; + createIndex(index, Settings.builder() + .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1) + .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, 0) + .build()); + + final long nbDocs = randomLongBetween(100, 1000); + try (BackgroundIndexer indexer = new BackgroundIndexer(index, "_doc", client(), (int) nbDocs)) { + waitForDocs(nbDocs, indexer); + } + + flushAndRefresh(index); + ForceMergeResponse forceMerge = client().admin().indices().prepareForceMerge(index).setFlush(true).setMaxNumSegments(1).get(); + assertThat(forceMerge.getSuccessfulShards(), equalTo(1)); + assertHitCount(client().prepareSearch(index).setSize(0).setTrackTotalHits(true).get(), nbDocs); + + final String snapshot = "snapshot"; + assertSuccessfulSnapshot(client().admin().cluster().prepareCreateSnapshot(repository, snapshot) + .setWaitForCompletion(true).setIndices(index)); + + assertAcked(client().admin().indices().prepareDelete(index)); + + assertSuccessfulRestore(client().admin().cluster().prepareRestoreSnapshot(repository, snapshot).setWaitForCompletion(true)); + ensureGreen(index); + assertHitCount(client().prepareSearch(index).setSize(0).setTrackTotalHits(true).get(), nbDocs); + + assertAcked(client().admin().cluster().prepareDeleteSnapshot(repository, snapshot).get()); + + final RepositoryStats repositoryStats = StreamSupport.stream( + internalCluster().getInstances(RepositoriesService.class).spliterator(), false) + .map(repositoriesService -> { + try { + return repositoriesService.repository(repository); + } catch (RepositoryMissingException e) { + return null; + } + }) + .filter(r -> r != null) + .map(r -> r.stats()) + .reduce((s1, s2) -> s1.merge(s2)) + .get(); + final long sdkGetCalls = repositoryStats.requestCounts.get("GET"); + final long sdkListCalls = repositoryStats.requestCounts.get("LIST"); + + final long getCalls = handlers.values().stream() + .mapToLong(h -> { + while (h instanceof DelegatingHttpHandler) { + if (h instanceof S3StatsHttpHandler) { + return ((S3StatsHttpHandler) h).getCalls.get(); + } + h = ((DelegatingHttpHandler) h).getDelegate(); + } + assert false; + return 0L; + }) + .sum(); + final long listCalls = handlers.values().stream() + .mapToLong(h -> { + while (h instanceof DelegatingHttpHandler) { + if (h instanceof S3StatsHttpHandler) { + return ((S3StatsHttpHandler) h).listCalls.get(); + } + h = ((DelegatingHttpHandler) h).getDelegate(); + } + assert false; + return 0L; + }) + .sum(); + + assertEquals("SDK sent " + sdkGetCalls + " GET calls and handler measured " + getCalls + " GET calls", getCalls, sdkGetCalls); + assertEquals("SDK sent " + sdkListCalls + " LIST calls and handler measured " + listCalls + " LIST calls", listCalls, sdkListCalls); + } + /** * S3RepositoryPlugin that allows to disable chunked encoding and to set a low threshold between single upload and multipart upload. */ @@ -266,4 +352,33 @@ public class S3BlobStoreRepositoryTests extends ESMockAPIBasedRepositoryIntegTes return exchange.getRequestHeaders().getFirst(AmazonHttpClient.HEADER_SDK_TRANSACTION_ID); } } + + @SuppressForbidden(reason = "this test uses a HttpServer to emulate an S3 endpoint") + private static class S3StatsHttpHandler implements DelegatingHttpHandler { + + private final HttpHandler delegate; + + public final AtomicLong getCalls = new AtomicLong(); + public final AtomicLong listCalls = new AtomicLong(); + + S3StatsHttpHandler(final HttpHandler delegate) { + this.delegate = delegate; + } + + @Override + public HttpHandler getDelegate() { + return delegate; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { + final String request = exchange.getRequestMethod() + " " + exchange.getRequestURI().toString(); + if (Regex.simpleMatch("GET /*/?prefix=*", request)) { + listCalls.incrementAndGet(); + } else if (Regex.simpleMatch("GET /*/*", request)) { + getCalls.incrementAndGet(); + } + delegate.handle(exchange); + } + } } diff --git a/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java b/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java index 6ed6722995c..9ac21d2aa3e 100644 --- a/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java +++ b/server/src/main/java/org/elasticsearch/common/blobstore/BlobStore.java @@ -19,6 +19,8 @@ package org.elasticsearch.common.blobstore; import java.io.Closeable; +import java.util.Collections; +import java.util.Map; /** * An interface for storing blobs. @@ -29,4 +31,11 @@ public interface BlobStore extends Closeable { * Get a blob container instance for storing blobs at the given {@link BlobPath}. */ BlobContainer blobContainer(BlobPath path); + + /** + * Returns statistics on the count of operations that have been performed on this blob store + */ + default Map stats() { + return Collections.emptyMap(); + } } diff --git a/server/src/main/java/org/elasticsearch/repositories/Repository.java b/server/src/main/java/org/elasticsearch/repositories/Repository.java index 5aab81d0077..ecd13a693bf 100644 --- a/server/src/main/java/org/elasticsearch/repositories/Repository.java +++ b/server/src/main/java/org/elasticsearch/repositories/Repository.java @@ -167,6 +167,12 @@ public interface Repository extends LifecycleComponent { */ long getRestoreThrottleTimeInNanos(); + /** + * Returns stats on the repository usage + */ + default RepositoryStats stats() { + return RepositoryStats.EMPTY_STATS; + } /** * Verifies repository on the master node and returns the verification token. diff --git a/server/src/main/java/org/elasticsearch/repositories/RepositoryStats.java b/server/src/main/java/org/elasticsearch/repositories/RepositoryStats.java new file mode 100644 index 00000000000..50a8b466301 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/repositories/RepositoryStats.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.repositories; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class RepositoryStats implements Writeable { + + public static final RepositoryStats EMPTY_STATS = new RepositoryStats(Collections.emptyMap()); + + public final Map requestCounts; + + public RepositoryStats(Map requestCounts) { + this.requestCounts = Collections.unmodifiableMap(requestCounts); + } + + public RepositoryStats(StreamInput in) throws IOException { + this.requestCounts = in.readMap(StreamInput::readString, StreamInput::readLong); + } + + public RepositoryStats merge(RepositoryStats otherStats) { + final Map result = new HashMap<>(); + result.putAll(requestCounts); + for (Map.Entry entry : otherStats.requestCounts.entrySet()) { + result.merge(entry.getKey(), entry.getValue(), Math::addExact); + } + return new RepositoryStats(result); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeMap(requestCounts, StreamOutput::writeString, StreamOutput::writeLong); + } +} diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 61f409e4355..3efc0a53649 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -104,6 +104,7 @@ import org.elasticsearch.repositories.RepositoryCleanupResult; import org.elasticsearch.repositories.RepositoryData; import org.elasticsearch.repositories.RepositoryException; import org.elasticsearch.repositories.RepositoryOperation; +import org.elasticsearch.repositories.RepositoryStats; import org.elasticsearch.repositories.RepositoryVerificationException; import org.elasticsearch.snapshots.SnapshotCreationException; import org.elasticsearch.repositories.ShardGenerations; @@ -486,6 +487,14 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp return metadata; } + public RepositoryStats stats() { + final BlobStore store = blobStore.get(); + if (store == null) { + return RepositoryStats.EMPTY_STATS; + } + return new RepositoryStats(store.stats()); + } + @Override public void initializeSnapshot(SnapshotId snapshotId, List indices, Metadata clusterMetadata) { try { @@ -501,6 +510,7 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp } } + @Override public void deleteSnapshot(SnapshotId snapshotId, long repositoryStateId, Version repositoryMetaVersion, ActionListener listener) { if (isReadOnly()) { diff --git a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESMockAPIBasedRepositoryIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESMockAPIBasedRepositoryIntegTestCase.java index 2bfe6729c21..bfabc85460f 100644 --- a/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESMockAPIBasedRepositoryIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/repositories/blobstore/ESMockAPIBasedRepositoryIntegTestCase.java @@ -43,6 +43,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -71,7 +72,7 @@ public abstract class ESMockAPIBasedRepositoryIntegTestCase extends ESBlobStoreR private static final byte[] BUFFER = new byte[1024]; private static HttpServer httpServer; - private Map handlers; + protected Map handlers; private static final Logger log = LogManager.getLogger(); @@ -91,8 +92,9 @@ public abstract class ESMockAPIBasedRepositoryIntegTestCase extends ESBlobStoreR @Before public void setUpHttpServer() { - handlers = createHttpHandlers(); - handlers.forEach((c, h) -> httpServer.createContext(c, wrap(randomBoolean() ? createErroneousHttpHandler(h) : h, logger))); + handlers = new HashMap<>(createHttpHandlers()); + handlers.replaceAll((k, h) -> wrap(randomBoolean() ? createErroneousHttpHandler(h) : h, logger)); + handlers.forEach(httpServer::createContext); } @AfterClass @@ -106,8 +108,12 @@ public abstract class ESMockAPIBasedRepositoryIntegTestCase extends ESBlobStoreR if (handlers != null) { for(Map.Entry handler : handlers.entrySet()) { httpServer.removeContext(handler.getKey()); - if (handler.getValue() instanceof BlobStoreHttpHandler) { - List blobs = ((BlobStoreHttpHandler) handler.getValue()).blobs().keySet().stream() + HttpHandler h = handler.getValue(); + while (h instanceof DelegatingHttpHandler) { + h = ((DelegatingHttpHandler) h).getDelegate(); + } + if (h instanceof BlobStoreHttpHandler) { + List blobs = ((BlobStoreHttpHandler) h).blobs().keySet().stream() .filter(blob -> blob.contains("index") == false).collect(Collectors.toList()); assertThat("Only index blobs should remain in repository but found " + blobs, blobs, hasSize(0)); } @@ -172,11 +178,12 @@ public abstract class ESMockAPIBasedRepositoryIntegTestCase extends ESBlobStoreR * slow down the test suite. */ @SuppressForbidden(reason = "this test uses a HttpServer to emulate a cloud-based storage service") - protected abstract static class ErroneousHttpHandler implements HttpHandler { + protected abstract static class ErroneousHttpHandler implements DelegatingHttpHandler { // first key is a unique identifier for the incoming HTTP request, // value is the number of times the request has been seen private final Map requests; + private final HttpHandler delegate; private final int maxErrorsPerRequest; @@ -227,13 +234,37 @@ public abstract class ESMockAPIBasedRepositoryIntegTestCase extends ESBlobStoreR protected boolean canFailRequest(final HttpExchange exchange) { return true; } + + public HttpHandler getDelegate() { + return delegate; + } + } + + @SuppressForbidden(reason = "this test uses a HttpServer to emulate a cloud-based storage service") + public interface DelegatingHttpHandler extends HttpHandler { + HttpHandler getDelegate(); } /** * Wrap a {@link HttpHandler} to log any thrown exception using the given {@link Logger}. */ - public static HttpHandler wrap(final HttpHandler handler, final Logger logger) { - return exchange -> { + public static DelegatingHttpHandler wrap(final HttpHandler handler, final Logger logger) { + return new ExceptionCatchingHttpHandler(handler, logger); + } + + @SuppressForbidden(reason = "this test uses a HttpServer to emulate a cloud-based storage service") + private static class ExceptionCatchingHttpHandler implements DelegatingHttpHandler { + + private final HttpHandler handler; + private final Logger logger; + + ExceptionCatchingHttpHandler(HttpHandler handler, Logger logger) { + this.handler = handler; + this.logger = logger; + } + + @Override + public void handle(HttpExchange exchange) throws IOException { try { handler.handle(exchange); } catch (Throwable t) { @@ -241,6 +272,11 @@ public abstract class ESMockAPIBasedRepositoryIntegTestCase extends ESBlobStoreR exchange.getRemoteAddress(), exchange.getRequestMethod(), exchange.getRequestURI()), t); throw t; } - }; + } + + @Override + public HttpHandler getDelegate() { + return handler; + } } } diff --git a/x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/repository_stats.yml b/x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/repository_stats.yml new file mode 100644 index 00000000000..d408b385643 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/repository_stats.yml @@ -0,0 +1,83 @@ +--- +setup: + + - do: + indices.create: + index: docs + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + + - do: + bulk: + body: + - index: + _index: docs + _id: 1 + - field: foo + - index: + _index: docs + _id: 2 + - field: bar + - index: + _index: docs + _id: 3 + - field: baz + + - do: + snapshot.create_repository: + repository: repository-fs + body: + type: fs + settings: + location: "repository-fs" + + # Remove the snapshot if a previous test failed to delete it. + # Useful for third party tests that runs the test against a real external service. + - do: + snapshot.delete: + repository: repository-fs + snapshot: snapshot + ignore: 404 + + - do: + snapshot.create: + repository: repository-fs + snapshot: snapshot + wait_for_completion: true + + - do: + indices.delete: + index: docs +--- +teardown: + + - do: + snapshot.delete: + repository: repository-fs + snapshot: snapshot + ignore: 404 + + - do: + snapshot.delete_repository: + repository: repository-fs + +--- +"Tests repository stats": + - skip: + version: " - 7.99.99" + reason: searchable snapshots introduced in 7.8.0 (8.0.0 currently, but adapt after backport to 7.x) + + - do: + snapshot.restore: + repository: repository-fs + snapshot: snapshot + wait_for_completion: true + + - do: + searchable_snapshots.repository_stats: + repository: repository-fs + + - is_true: _all + - is_true: nodes diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java index 35ea168dfd9..9f46911a59b 100644 --- a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshots.java @@ -49,13 +49,16 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.searchablesnapshots.MountSearchableSnapshotAction; import org.elasticsearch.xpack.searchablesnapshots.action.ClearSearchableSnapshotsCacheAction; +import org.elasticsearch.xpack.searchablesnapshots.action.RepositoryStatsAction; import org.elasticsearch.xpack.searchablesnapshots.action.SearchableSnapshotsStatsAction; import org.elasticsearch.xpack.searchablesnapshots.action.TransportClearSearchableSnapshotsCacheAction; import org.elasticsearch.xpack.searchablesnapshots.action.TransportMountSearchableSnapshotAction; +import org.elasticsearch.xpack.searchablesnapshots.action.TransportRepositoryStatsAction; import org.elasticsearch.xpack.searchablesnapshots.action.TransportSearchableSnapshotsStatsAction; import org.elasticsearch.xpack.searchablesnapshots.cache.CacheService; import org.elasticsearch.xpack.searchablesnapshots.rest.RestClearSearchableSnapshotsCacheAction; import org.elasticsearch.xpack.searchablesnapshots.rest.RestMountSearchableSnapshotAction; +import org.elasticsearch.xpack.searchablesnapshots.rest.RestRepositoryStatsAction; import org.elasticsearch.xpack.searchablesnapshots.rest.RestSearchableSnapshotsStatsAction; import java.util.Collection; @@ -234,7 +237,8 @@ public class SearchableSnapshots extends Plugin implements IndexStorePlugin, Rep return org.elasticsearch.common.collect.List.of( new ActionHandler<>(SearchableSnapshotsStatsAction.INSTANCE, TransportSearchableSnapshotsStatsAction.class), new ActionHandler<>(ClearSearchableSnapshotsCacheAction.INSTANCE, TransportClearSearchableSnapshotsCacheAction.class), - new ActionHandler<>(MountSearchableSnapshotAction.INSTANCE, TransportMountSearchableSnapshotAction.class) + new ActionHandler<>(MountSearchableSnapshotAction.INSTANCE, TransportMountSearchableSnapshotAction.class), + new ActionHandler<>(RepositoryStatsAction.INSTANCE, TransportRepositoryStatsAction.class) ); } else { return org.elasticsearch.common.collect.List.of(); @@ -254,7 +258,8 @@ public class SearchableSnapshots extends Plugin implements IndexStorePlugin, Rep return org.elasticsearch.common.collect.List.of( new RestSearchableSnapshotsStatsAction(), new RestClearSearchableSnapshotsCacheAction(), - new RestMountSearchableSnapshotAction() + new RestMountSearchableSnapshotAction(), + new RestRepositoryStatsAction() ); } else { return org.elasticsearch.common.collect.List.of(); diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsAction.java new file mode 100644 index 00000000000..0636f970422 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsAction.java @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.ActionType; + +public class RepositoryStatsAction extends ActionType { + + public static final RepositoryStatsAction INSTANCE = new RepositoryStatsAction(); + public static final String NAME = "cluster:admin/repository/stats"; + + private RepositoryStatsAction() { + super(NAME, RepositoryStatsResponse::new); + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsNodeRequest.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsNodeRequest.java new file mode 100644 index 00000000000..459678c9ab1 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsNodeRequest.java @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.support.nodes.BaseNodeRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +public class RepositoryStatsNodeRequest extends BaseNodeRequest { + + private final String repository; + + public RepositoryStatsNodeRequest(String repository) { + this.repository = repository; + } + + public RepositoryStatsNodeRequest(StreamInput in) throws IOException { + super(in); + this.repository = in.readString(); + } + + public String getRepository() { + return repository; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(repository); + } + +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsNodeResponse.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsNodeResponse.java new file mode 100644 index 00000000000..ddf3023ceae --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsNodeResponse.java @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.support.nodes.BaseNodeResponse; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.repositories.RepositoryStats; + +import java.io.IOException; + +public class RepositoryStatsNodeResponse extends BaseNodeResponse implements ToXContentObject { + + private final RepositoryStats repositoryStats; + + public RepositoryStatsNodeResponse(StreamInput in) throws IOException { + super(in); + repositoryStats = new RepositoryStats(in); + } + + public RepositoryStatsNodeResponse(DiscoveryNode node, RepositoryStats repositoryStats) { + super(node); + this.repositoryStats = repositoryStats; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + repositoryStats.writeTo(out); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (repositoryStats.requestCounts.isEmpty() == false) { + builder.field("stats", repositoryStats.requestCounts); + } + builder.endObject(); + return builder; + } + + public RepositoryStats getRepositoryStats() { + return repositoryStats; + } + +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsRequest.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsRequest.java new file mode 100644 index 00000000000..a75ba492fb4 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsRequest.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.support.nodes.BaseNodesRequest; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +public class RepositoryStatsRequest extends BaseNodesRequest { + + private final String repository; + + public RepositoryStatsRequest(StreamInput in) throws IOException { + super(in); + repository = in.readString(); + } + + public RepositoryStatsRequest(String repository, String... nodesIds) { + super(nodesIds); + this.repository = repository; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(repository); + } + + public String getRepository() { + return repository; + } + +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsResponse.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsResponse.java new file mode 100644 index 00000000000..97a6887fbbb --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/RepositoryStatsResponse.java @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.nodes.BaseNodesResponse; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.repositories.RepositoryStats; + +import java.io.IOException; +import java.util.List; + +public class RepositoryStatsResponse extends BaseNodesResponse implements ToXContentObject { + + private final RepositoryStats globalStats; + + public RepositoryStatsResponse(ClusterName clusterName, List nodes, List failures) { + super(clusterName, nodes, failures); + globalStats = computeGlobalStats(getNodes()); + } + + public RepositoryStatsResponse(StreamInput in) throws IOException { + super(in); + globalStats = computeGlobalStats(getNodes()); + } + + private static RepositoryStats computeGlobalStats(List nodes) { + if (nodes.isEmpty()) { + return RepositoryStats.EMPTY_STATS; + } else { + return nodes.stream().map(RepositoryStatsNodeResponse::getRepositoryStats).reduce(RepositoryStats::merge).get(); + } + } + + @Override + protected List readNodesFrom(StreamInput in) throws IOException { + return in.readList(RepositoryStatsNodeResponse::new); + } + + @Override + protected void writeNodesTo(StreamOutput out, List nodes) throws IOException { + out.writeList(nodes); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("_all", globalStats.requestCounts); + builder.startArray("nodes"); + for (RepositoryStatsNodeResponse node : getNodes()) { + node.toXContent(builder, params); + } + builder.endArray(); + builder.endObject(); + + return builder; + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportRepositoryStatsAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportRepositoryStatsAction.java new file mode 100644 index 00000000000..b02ed80b2cf --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportRepositoryStatsAction.java @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.action; + +import org.elasticsearch.action.FailedNodeException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.nodes.TransportNodesAction; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.repositories.RepositoriesService; +import org.elasticsearch.repositories.Repository; +import org.elasticsearch.repositories.RepositoryStats; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.searchablesnapshots.SearchableSnapshots; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; + +public class TransportRepositoryStatsAction extends TransportNodesAction< + RepositoryStatsRequest, + RepositoryStatsResponse, + RepositoryStatsNodeRequest, + RepositoryStatsNodeResponse> { + + private final RepositoriesService repositoriesService; + private final XPackLicenseState licenseState; + + @Inject + public TransportRepositoryStatsAction( + ThreadPool threadPool, + ClusterService clusterService, + TransportService transportService, + ActionFilters actionFilters, + RepositoriesService repositoriesService, + XPackLicenseState licenseState + ) { + super( + RepositoryStatsAction.NAME, + threadPool, + clusterService, + transportService, + actionFilters, + RepositoryStatsRequest::new, + RepositoryStatsNodeRequest::new, + ThreadPool.Names.SAME, + RepositoryStatsNodeResponse.class + ); + this.repositoriesService = repositoriesService; + this.licenseState = Objects.requireNonNull(licenseState); + } + + @Override + protected RepositoryStatsResponse newResponse( + RepositoryStatsRequest request, + List nodes, + List failures + ) { + return new RepositoryStatsResponse(clusterService.getClusterName(), nodes, failures); + } + + @Override + protected RepositoryStatsNodeRequest newNodeRequest(RepositoryStatsRequest request) { + return new RepositoryStatsNodeRequest(request.getRepository()); + } + + @Override + protected RepositoryStatsNodeResponse newNodeResponse(StreamInput in) throws IOException { + return new RepositoryStatsNodeResponse(in); + } + + @Override + protected RepositoryStatsNodeResponse nodeOperation(RepositoryStatsNodeRequest request) { + SearchableSnapshots.ensureValidLicense(licenseState); + if (clusterService.localNode().isMasterNode() == false && clusterService.localNode().isDataNode() == false) { + return new RepositoryStatsNodeResponse(clusterService.localNode(), RepositoryStats.EMPTY_STATS); + } + final Repository repository = repositoriesService.repository(request.getRepository()); + return new RepositoryStatsNodeResponse(clusterService.localNode(), repository.stats()); + } +} diff --git a/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestRepositoryStatsAction.java b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestRepositoryStatsAction.java new file mode 100644 index 00000000000..e11eb654857 --- /dev/null +++ b/x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestRepositoryStatsAction.java @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.searchablesnapshots.rest; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.searchablesnapshots.action.RepositoryStatsAction; +import org.elasticsearch.xpack.searchablesnapshots.action.RepositoryStatsRequest; + +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class RestRepositoryStatsAction extends BaseRestHandler { + + @Override + public String getName() { + return "repository_stats_action"; + } + + @Override + public List routes() { + return Collections.singletonList(new RestHandler.Route(GET, "/_snapshot/{repository}/_stats")); + } + + @Override + protected BaseRestHandler.RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + final RepositoryStatsRequest repositoryStatsRequest = new RepositoryStatsRequest(request.param("repository")); + return channel -> client.execute(RepositoryStatsAction.INSTANCE, repositoryStatsRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/searchable_snapshots.repository_stats.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/searchable_snapshots.repository_stats.json new file mode 100644 index 00000000000..9f34c76691f --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/searchable_snapshots.repository_stats.json @@ -0,0 +1,24 @@ +{ + "searchable_snapshots.repository_stats": { + "documentation": { + "url": "https://www.elastic.co/guide/en/elasticsearch/reference/master/searchable-snapshots-repository-stats.html" + }, + "stability": "experimental", + "url": { + "paths": [ + { + "path": "/_snapshot/{repository}/_stats", + "methods": [ + "GET" + ], + "parts": { + "repository": { + "type": "string", + "description": "The repository for which to get the stats for" + } + } + } + ] + } + } +}