Browse Source

Regroup searchable snapshot stats by file extension (#68835)

The searchable snapshots stats endpoint currently exposes stats information at the level of individual files, which can be
extremely verbose for large indices. Furthermore, the tracking at the per-file level consumes a non-negligible amount of
heap memory.

With this change, the stats are now aggregated per file extension, and can be aggregated both at the index as well as
cluster-level. It introduces a new "level" parameter, similar to indices stats, which defaults to "indices", but can also be
set to "cluster" or "shards", provide details at the respective level.

The commit also fixes a small issue where the backwardsSeeks stats were reported as negative values instead of
positive ones.
Yannick Welsch 4 years ago
parent
commit
6cf33b1c7c
16 changed files with 449 additions and 118 deletions
  1. 96 35
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java
  2. 1 1
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java
  3. 170 2
      x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/stats.yml
  4. 2 2
      x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/blobstore/cache/SearchableSnapshotsBlobStoreCacheIntegTests.java
  5. 13 14
      x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsIntegTests.java
  6. 15 9
      x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/index/store/IndexInputStats.java
  7. 20 5
      x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/index/store/SearchableSnapshotDirectory.java
  8. 84 27
      x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponse.java
  9. 4 3
      x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java
  10. 9 0
      x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestSearchableSnapshotsStatsAction.java
  11. 8 8
      x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/index/store/IndexInputStatsTests.java
  12. 10 10
      x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/index/store/SearchableSnapshotDirectoryStatsTests.java
  13. 1 1
      x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/index/store/direct/DirectBlobContainerIndexInputTests.java
  14. 3 1
      x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/AbstractSearchableSnapshotsRestTestCase.java
  15. 1 0
      x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponseTests.java
  16. 12 0
      x-pack/plugin/src/test/resources/rest-api-spec/api/searchable_snapshots.stats.json

+ 96 - 35
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStats.java

@@ -6,6 +6,7 @@
  */
 package org.elasticsearch.xpack.core.searchablesnapshots;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.cluster.routing.ShardRouting;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
@@ -90,7 +91,7 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
             builder.startArray("files");
             {
                 List<CacheIndexInputStats> stats = inputStats.stream()
-                    .sorted(Comparator.comparing(CacheIndexInputStats::getFileName)).collect(toList());
+                    .sorted(Comparator.comparing(CacheIndexInputStats::getFileExt)).collect(toList());
                 for (CacheIndexInputStats stat : stats) {
                     stat.toXContent(builder, params);
                 }
@@ -122,8 +123,9 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
 
     public static class CacheIndexInputStats implements Writeable, ToXContentObject {
 
-        private final String fileName;
-        private final long fileLength;
+        private final String fileExt;
+        private final long numFiles;
+        private final long totalSize;
 
         private final long openCount;
         private final long closeCount;
@@ -142,15 +144,16 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
         private final Counter blobStoreBytesRequested;
         private final long currentIndexCacheFills;
 
-        public CacheIndexInputStats(String fileName, long fileLength, long openCount, long closeCount,
+        public CacheIndexInputStats(String fileExt, long numFiles, long totalSize, long openCount, long closeCount,
                                     Counter forwardSmallSeeks, Counter backwardSmallSeeks,
                                     Counter forwardLargeSeeks, Counter backwardLargeSeeks,
                                     Counter contiguousReads, Counter nonContiguousReads,
                                     Counter cachedBytesRead, Counter indexCacheBytesRead,
                                     TimedCounter cachedBytesWritten, TimedCounter directBytesRead, TimedCounter optimizedBytesRead,
                                     Counter blobStoreBytesRequested, long currentIndexCacheFills) {
-            this.fileName = fileName;
-            this.fileLength = fileLength;
+            this.fileExt = fileExt;
+            this.numFiles = numFiles;
+            this.totalSize = totalSize;
             this.openCount = openCount;
             this.closeCount = closeCount;
             this.forwardSmallSeeks = forwardSmallSeeks;
@@ -169,8 +172,15 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
         }
 
         CacheIndexInputStats(final StreamInput in) throws IOException {
-            this.fileName = in.readString();
-            this.fileLength = in.readVLong();
+            if (in.getVersion().before(Version.V_7_12_0)) {
+                // This API is currently only used internally for testing, so BWC breaking changes are OK.
+                // We just throw an exception here to get a better error message in case this would be called
+                // in a mixed version cluster
+                throw new IllegalArgumentException("BWC breaking change for internal API");
+            }
+            this.fileExt = in.readString();
+            this.numFiles = in.readVLong();
+            this.totalSize = in.readVLong();
             this.openCount = in.readVLong();
             this.closeCount = in.readVLong();
             this.forwardSmallSeeks = new Counter(in);
@@ -188,10 +198,45 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
             this.currentIndexCacheFills = in.readVLong();
         }
 
+        public static CacheIndexInputStats combine(CacheIndexInputStats cis1, CacheIndexInputStats cis2) {
+            if (cis1.getFileExt().equals(cis2.getFileExt()) == false) {
+                assert false : "can only combine same file extensions";
+                throw new IllegalArgumentException("can only combine same file extensions but was " +
+                    cis1.fileExt + " and " + cis2.fileExt);
+            }
+            return new CacheIndexInputStats(
+                cis1.fileExt,
+                cis1.numFiles + cis2.numFiles,
+                cis1.totalSize + cis2.totalSize,
+                cis1.openCount + cis2.openCount,
+                cis1.closeCount + cis2.closeCount,
+                cis1.forwardSmallSeeks.add(cis2.forwardSmallSeeks),
+                cis1.backwardSmallSeeks.add(cis2.backwardSmallSeeks),
+                cis1.forwardLargeSeeks.add(cis2.forwardLargeSeeks),
+                cis1.backwardLargeSeeks.add(cis2.backwardLargeSeeks),
+                cis1.contiguousReads.add(cis2.contiguousReads),
+                cis1.nonContiguousReads.add(cis2.nonContiguousReads),
+                cis1.cachedBytesRead.add(cis2.cachedBytesRead),
+                cis1.indexCacheBytesRead.add(cis2.indexCacheBytesRead),
+                cis1.cachedBytesWritten.add(cis2.cachedBytesWritten),
+                cis1.directBytesRead.add(cis2.directBytesRead),
+                cis1.optimizedBytesRead.add(cis2.optimizedBytesRead),
+                cis1.blobStoreBytesRequested.add(cis2.blobStoreBytesRequested),
+                cis1.currentIndexCacheFills + cis2.currentIndexCacheFills
+            );
+        }
+
         @Override
         public void writeTo(StreamOutput out) throws IOException {
-            out.writeString(fileName);
-            out.writeVLong(fileLength);
+            if (out.getVersion().before(Version.V_7_12_0)) {
+                // This API is currently only used internally for testing, so BWC breaking changes are OK.
+                // We just throw an exception here to get a better error message in case this would be called
+                // in a mixed version cluster
+                throw new IllegalArgumentException("BWC breaking change for internal API");
+            }
+            out.writeString(fileExt);
+            out.writeVLong(numFiles);
+            out.writeVLong(totalSize);
             out.writeVLong(openCount);
             out.writeVLong(closeCount);
 
@@ -210,12 +255,16 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
             out.writeVLong(currentIndexCacheFills);
         }
 
-        public String getFileName() {
-            return fileName;
+        public String getFileExt() {
+            return fileExt;
         }
 
-        public long getFileLength() {
-            return fileLength;
+        public long getNumFiles() {
+            return numFiles;
+        }
+
+        public long getTotalSize() {
+            return totalSize;
         }
 
         public long getOpenCount() {
@@ -282,30 +331,31 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
         public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
             builder.startObject();
             {
-                builder.field("name", getFileName());
-                builder.field("length", getFileLength());
+                builder.field("file_ext", getFileExt());
+                builder.field("num_files", getNumFiles());
+                builder.field("total_size", getTotalSize());
                 builder.field("open_count", getOpenCount());
                 builder.field("close_count", getCloseCount());
-                builder.field("contiguous_bytes_read", getContiguousReads());
-                builder.field("non_contiguous_bytes_read", getNonContiguousReads());
-                builder.field("cached_bytes_read", getCachedBytesRead());
-                builder.field("index_cache_bytes_read", getIndexCacheBytesRead());
-                builder.field("cached_bytes_written", getCachedBytesWritten());
-                builder.field("direct_bytes_read", getDirectBytesRead());
-                builder.field("optimized_bytes_read", getOptimizedBytesRead());
+                builder.field("contiguous_bytes_read", getContiguousReads(), params);
+                builder.field("non_contiguous_bytes_read", getNonContiguousReads(), params);
+                builder.field("cached_bytes_read", getCachedBytesRead(), params);
+                builder.field("index_cache_bytes_read", getIndexCacheBytesRead(), params);
+                builder.field("cached_bytes_written", getCachedBytesWritten(), params);
+                builder.field("direct_bytes_read", getDirectBytesRead(), params);
+                builder.field("optimized_bytes_read", getOptimizedBytesRead(), params);
                 {
                     builder.startObject("forward_seeks");
-                    builder.field("small", getForwardSmallSeeks());
-                    builder.field("large", getForwardLargeSeeks());
+                    builder.field("small", getForwardSmallSeeks(), params);
+                    builder.field("large", getForwardLargeSeeks(), params);
                     builder.endObject();
                 }
                 {
                     builder.startObject("backward_seeks");
-                    builder.field("small", getBackwardSmallSeeks());
-                    builder.field("large", getBackwardLargeSeeks());
+                    builder.field("small", getBackwardSmallSeeks(), params);
+                    builder.field("large", getBackwardLargeSeeks(), params);
                     builder.endObject();
                 }
-                builder.field("blob_store_bytes_requested", getBlobStoreBytesRequested());
+                builder.field("blob_store_bytes_requested", getBlobStoreBytesRequested(), params);
                 builder.field("current_index_cache_fills", getCurrentIndexCacheFills());
             }
             return builder.endObject();
@@ -320,10 +370,11 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
                 return false;
             }
             CacheIndexInputStats stats = (CacheIndexInputStats) other;
-            return fileLength == stats.fileLength
+            return numFiles == stats.numFiles
+                && totalSize == stats.totalSize
                 && openCount == stats.openCount
                 && closeCount == stats.closeCount
-                && Objects.equals(fileName, stats.fileName)
+                && Objects.equals(fileExt, stats.fileExt)
                 && Objects.equals(forwardSmallSeeks, stats.forwardSmallSeeks)
                 && Objects.equals(backwardSmallSeeks, stats.backwardSmallSeeks)
                 && Objects.equals(forwardLargeSeeks, stats.forwardLargeSeeks)
@@ -341,7 +392,7 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
 
         @Override
         public int hashCode() {
-            return Objects.hash(fileName, fileLength, openCount, closeCount,
+            return Objects.hash(fileExt, numFiles, totalSize, openCount, closeCount,
                 forwardSmallSeeks, backwardSmallSeeks,
                 forwardLargeSeeks, backwardLargeSeeks,
                 contiguousReads, nonContiguousReads,
@@ -353,10 +404,10 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
 
     public static class Counter implements Writeable, ToXContentObject {
 
-        private final long count;
-        private final long total;
-        private final long min;
-        private final long max;
+        protected final long count;
+        protected final long total;
+        protected final long min;
+        protected final long max;
 
         public Counter(final long count, final long total, final long min, final long max) {
             this.count = count;
@@ -372,6 +423,11 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
             this.max = in.readZLong();
         }
 
+        public Counter add(Counter counter) {
+            return new Counter(count + counter.count, total + counter.total,
+                Math.min(min, counter.min), Math.max(max, counter.max));
+        }
+
         @Override
         public void writeTo(final StreamOutput out) throws IOException {
             out.writeZLong(count);
@@ -448,6 +504,11 @@ public class SearchableSnapshotShardStats implements Writeable, ToXContentObject
             totalNanoseconds = in.readZLong();
         }
 
+        public TimedCounter add(TimedCounter counter) {
+            return new TimedCounter(count + counter.count, total + counter.total,
+                Math.min(min, counter.min), Math.max(max, counter.max), totalNanoseconds + counter.totalNanoseconds);
+        }
+
         @Override
         public void writeTo(StreamOutput out) throws IOException {
             super.writeTo(out);

+ 1 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/searchablesnapshots/SearchableSnapshotShardStatsTests.java

@@ -42,7 +42,7 @@ public class SearchableSnapshotShardStatsTests extends AbstractWireSerializingTe
     }
 
     private CacheIndexInputStats randomCacheIndexInputStats() {
-        return new CacheIndexInputStats(randomAlphaOfLength(10), randomNonNegativeLong(),
+        return new CacheIndexInputStats(randomAlphaOfLength(10), randomNonNegativeLong(), randomNonNegativeLong(),
             randomNonNegativeLong(), randomNonNegativeLong(),
             randomCounter(), randomCounter(),
             randomCounter(), randomCounter(),

+ 170 - 2
x-pack/plugin/searchable-snapshots/qa/rest/src/test/resources/rest-api-spec/test/stats.yml

@@ -131,11 +131,160 @@ teardown:
   - do:
       searchable_snapshots.stats:
         index: "d*"
+        level: "shards"
 
   - match:  { _shards.total: 1 }
   - match:  { _shards.failed: 0 }
 
+  - is_true: total
+  - is_true: total.0.file_ext
+  - gt:      { total.0.num_files: 0 }
+  - gt:      { total.0.total_size: 0 }
+  - gt:      { total.0.open_count: 0 }
+  - gt:      { total.0.close_count: 0 }
+
+  - gte:     { total.0.contiguous_bytes_read.count: 0 }
+  - gte:     { total.0.contiguous_bytes_read.sum: 0 }
+  - gte:     { total.0.contiguous_bytes_read.min: 0 }
+  - gte:     { total.0.contiguous_bytes_read.max: 0 }
+
+  - gte:     { total.0.non_contiguous_bytes_read.count: 0 }
+  - gte:     { total.0.non_contiguous_bytes_read.sum: 0 }
+  - gte:     { total.0.non_contiguous_bytes_read.min: 0 }
+  - gte:     { total.0.non_contiguous_bytes_read.max: 0 }
+
+  - gte:     { total.0.cached_bytes_read.count: 0 }
+  - gte:     { total.0.cached_bytes_read.sum: 0 }
+  - gte:     { total.0.cached_bytes_read.min: 0 }
+  - gte:     { total.0.cached_bytes_read.max: 0 }
+
+  - gte:     { total.0.index_cache_bytes_read.count: 0 }
+  - gte:     { total.0.index_cache_bytes_read.sum: 0 }
+  - gte:     { total.0.index_cache_bytes_read.min: 0 }
+  - gte:     { total.0.index_cache_bytes_read.max: 0 }
+
+  - gte:     { total.0.cached_bytes_written.count: 0 }
+  - gte:     { total.0.cached_bytes_written.sum: 0 }
+  - gte:     { total.0.cached_bytes_written.min: 0 }
+  - gte:     { total.0.cached_bytes_written.max: 0 }
+  - gte:     { total.0.cached_bytes_written.time_in_nanos: 0 }
+  - is_false:  total.0.cached_bytes_written.time
+
+  - gte:     { total.0.direct_bytes_read.count: 0 }
+  - gte:     { total.0.direct_bytes_read.sum: 0 }
+  - gte:     { total.0.direct_bytes_read.min: 0 }
+  - gte:     { total.0.direct_bytes_read.max: 0 }
+  - gte:     { total.0.direct_bytes_read.time_in_nanos: 0 }
+  - is_false:  total.0.direct_bytes_read.time
+
+  - gte:     { total.0.optimized_bytes_read.count: 0 }
+  - gte:     { total.0.optimized_bytes_read.sum: 0 }
+  - gte:     { total.0.optimized_bytes_read.min: 0 }
+  - gte:     { total.0.optimized_bytes_read.max: 0 }
+  - gte:     { total.0.optimized_bytes_read.time_in_nanos: 0 }
+  - is_false:  total.0.optimized_bytes_read.time
+
+  - gte:     { total.0.forward_seeks.small.count: 0 }
+  - gte:     { total.0.forward_seeks.small.sum: 0 }
+  - gte:     { total.0.forward_seeks.small.min: 0 }
+  - gte:     { total.0.forward_seeks.small.max: 0 }
+  - gte:     { total.0.forward_seeks.large.count: 0 }
+  - gte:     { total.0.forward_seeks.large.sum: 0 }
+  - gte:     { total.0.forward_seeks.large.min: 0 }
+  - gte:     { total.0.forward_seeks.large.max: 0 }
+
+  - gte:     { total.0.backward_seeks.small.count: 0 }
+  - gte:     { total.0.backward_seeks.small.sum: 0 }
+  - gte:     { total.0.backward_seeks.small.min: 0 }
+  - gte:     { total.0.backward_seeks.small.max: 0 }
+  - gte:     { total.0.backward_seeks.large.count: 0 }
+  - gte:     { total.0.backward_seeks.large.sum: 0 }
+  - gte:     { total.0.backward_seeks.large.min: 0 }
+  - gte:     { total.0.backward_seeks.large.max: 0 }
+
+  - gte:     { total.0.blob_store_bytes_requested.count: 0 }
+  - gte:     { total.0.blob_store_bytes_requested.sum: 0 }
+  - gte:     { total.0.blob_store_bytes_requested.min: 0 }
+  - gte:     { total.0.blob_store_bytes_requested.max: 0 }
+
+  - gte:     { total.0.current_index_cache_fills: 0 }
+
   - length:  { indices: 1 }
+  - is_true: indices.docs
+  - is_true: indices.docs.total
+
+  - is_true: indices.docs.total.0.file_ext
+  - gt:      { indices.docs.total.0.num_files: 0 }
+  - gt:      { indices.docs.total.0.total_size: 0 }
+  - gt:      { indices.docs.total.0.open_count: 0 }
+  - gt:      { indices.docs.total.0.close_count: 0 }
+
+  - gte:     { indices.docs.total.0.contiguous_bytes_read.count: 0 }
+  - gte:     { indices.docs.total.0.contiguous_bytes_read.sum: 0 }
+  - gte:     { indices.docs.total.0.contiguous_bytes_read.min: 0 }
+  - gte:     { indices.docs.total.0.contiguous_bytes_read.max: 0 }
+
+  - gte:     { indices.docs.total.0.non_contiguous_bytes_read.count: 0 }
+  - gte:     { indices.docs.total.0.non_contiguous_bytes_read.sum: 0 }
+  - gte:     { indices.docs.total.0.non_contiguous_bytes_read.min: 0 }
+  - gte:     { indices.docs.total.0.non_contiguous_bytes_read.max: 0 }
+
+  - gte:     { indices.docs.total.0.cached_bytes_read.count: 0 }
+  - gte:     { indices.docs.total.0.cached_bytes_read.sum: 0 }
+  - gte:     { indices.docs.total.0.cached_bytes_read.min: 0 }
+  - gte:     { indices.docs.total.0.cached_bytes_read.max: 0 }
+
+  - gte:     { indices.docs.total.0.index_cache_bytes_read.count: 0 }
+  - gte:     { indices.docs.total.0.index_cache_bytes_read.sum: 0 }
+  - gte:     { indices.docs.total.0.index_cache_bytes_read.min: 0 }
+  - gte:     { indices.docs.total.0.index_cache_bytes_read.max: 0 }
+
+  - gte:     { indices.docs.total.0.cached_bytes_written.count: 0 }
+  - gte:     { indices.docs.total.0.cached_bytes_written.sum: 0 }
+  - gte:     { indices.docs.total.0.cached_bytes_written.min: 0 }
+  - gte:     { indices.docs.total.0.cached_bytes_written.max: 0 }
+  - gte:     { indices.docs.total.0.cached_bytes_written.time_in_nanos: 0 }
+  - is_false:  indices.docs.total.0.cached_bytes_written.time
+
+  - gte:     { indices.docs.total.0.direct_bytes_read.count: 0 }
+  - gte:     { indices.docs.total.0.direct_bytes_read.sum: 0 }
+  - gte:     { indices.docs.total.0.direct_bytes_read.min: 0 }
+  - gte:     { indices.docs.total.0.direct_bytes_read.max: 0 }
+  - gte:     { indices.docs.total.0.direct_bytes_read.time_in_nanos: 0 }
+  - is_false:  indices.docs.total.0.direct_bytes_read.time
+
+  - gte:     { indices.docs.total.0.optimized_bytes_read.count: 0 }
+  - gte:     { indices.docs.total.0.optimized_bytes_read.sum: 0 }
+  - gte:     { indices.docs.total.0.optimized_bytes_read.min: 0 }
+  - gte:     { indices.docs.total.0.optimized_bytes_read.max: 0 }
+  - gte:     { indices.docs.total.0.optimized_bytes_read.time_in_nanos: 0 }
+  - is_false:  indices.docs.total.0.optimized_bytes_read.time
+
+  - gte:     { indices.docs.total.0.forward_seeks.small.count: 0 }
+  - gte:     { indices.docs.total.0.forward_seeks.small.sum: 0 }
+  - gte:     { indices.docs.total.0.forward_seeks.small.min: 0 }
+  - gte:     { indices.docs.total.0.forward_seeks.small.max: 0 }
+  - gte:     { indices.docs.total.0.forward_seeks.large.count: 0 }
+  - gte:     { indices.docs.total.0.forward_seeks.large.sum: 0 }
+  - gte:     { indices.docs.total.0.forward_seeks.large.min: 0 }
+  - gte:     { indices.docs.total.0.forward_seeks.large.max: 0 }
+
+  - gte:     { indices.docs.total.0.backward_seeks.small.count: 0 }
+  - gte:     { indices.docs.total.0.backward_seeks.small.sum: 0 }
+  - gte:     { indices.docs.total.0.backward_seeks.small.min: 0 }
+  - gte:     { indices.docs.total.0.backward_seeks.small.max: 0 }
+  - gte:     { indices.docs.total.0.backward_seeks.large.count: 0 }
+  - gte:     { indices.docs.total.0.backward_seeks.large.sum: 0 }
+  - gte:     { indices.docs.total.0.backward_seeks.large.min: 0 }
+  - gte:     { indices.docs.total.0.backward_seeks.large.max: 0 }
+
+  - gte:     { indices.docs.total.0.blob_store_bytes_requested.count: 0 }
+  - gte:     { indices.docs.total.0.blob_store_bytes_requested.sum: 0 }
+  - gte:     { indices.docs.total.0.blob_store_bytes_requested.min: 0 }
+  - gte:     { indices.docs.total.0.blob_store_bytes_requested.max: 0 }
+
+  - gte:     { indices.docs.total.0.current_index_cache_fills: 0 }
+
   - length:  { indices.docs.shards: 1 }
   - length:  { indices.docs.shards.0: 1 }
   - is_true: indices.docs.shards.0.0.snapshot_uuid
@@ -144,8 +293,9 @@ teardown:
   - match:   { indices.docs.shards.0.0.shard.primary: true }
   - match:   { indices.docs.shards.0.0.shard.node: $node_id }
 
-  - is_true: indices.docs.shards.0.0.files.0.name
-  - gt:      { indices.docs.shards.0.0.files.0.length: 0 }
+  - is_true: indices.docs.shards.0.0.files.0.file_ext
+  - gt:      { indices.docs.shards.0.0.files.0.num_files: 0 }
+  - gt:      { indices.docs.shards.0.0.files.0.total_size: 0 }
   - gt:      { indices.docs.shards.0.0.files.0.open_count: 0 }
   - gt:      { indices.docs.shards.0.0.files.0.close_count: 0 }
 
@@ -218,7 +368,25 @@ teardown:
   - do:
       searchable_snapshots.stats:
         index: "d*"
+        level: "shards"
         human: true
 
   - is_true:   indices.docs.shards.0.0.files.0.cached_bytes_written.time
   - is_true:   indices.docs.shards.0.0.files.0.direct_bytes_read.time
+
+  - do:
+      searchable_snapshots.stats:
+        index: "d*"
+        level: "cluster"
+
+  - is_true: total.0.file_ext
+  - is_false: indices.docs.total.0.file_ext
+  - is_false: indices.docs.shards.0.0.files.0.file_ext
+
+  - do:
+      searchable_snapshots.stats:
+        index: "d*"
+
+  - is_true: total.0.file_ext
+  - is_true: indices.docs.total.0.file_ext
+  - is_false: indices.docs.shards.0.0.files.0.file_ext

+ 2 - 2
x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/blobstore/cache/SearchableSnapshotsBlobStoreCacheIntegTests.java

@@ -224,8 +224,8 @@ public class SearchableSnapshotsBlobStoreCacheIntegTests extends BaseSearchableS
         ).actionGet().getStats()) {
             for (final SearchableSnapshotShardStats.CacheIndexInputStats indexInputStats : shardStats.getStats()) {
                 // we read the header of each file contained within the .cfs file, which could be anywhere
-                final boolean mayReadMoreThanHeader = indexInputStats.getFileName().endsWith(".cfs");
-                if (indexInputStats.getFileLength() <= BlobStoreCacheService.DEFAULT_CACHED_BLOB_SIZE * 2
+                final boolean mayReadMoreThanHeader = indexInputStats.getFileExt().equals("cfs");
+                if (indexInputStats.getTotalSize() <= BlobStoreCacheService.DEFAULT_CACHED_BLOB_SIZE * 2
                     || mayReadMoreThanHeader == false) {
                     assertThat(Strings.toString(indexInputStats), indexInputStats.getBlobStoreBytesRequested().getCount(), equalTo(0L));
                 }

+ 13 - 14
x-pack/plugin/searchable-snapshots/src/internalClusterTest/java/org/elasticsearch/xpack/searchablesnapshots/SearchableSnapshotsIntegTests.java

@@ -7,7 +7,6 @@
 package org.elasticsearch.xpack.searchablesnapshots;
 
 import com.carrotsearch.hppc.cursors.ObjectCursor;
-import org.apache.lucene.index.IndexFileNames;
 import org.apache.lucene.search.TotalHits;
 import org.elasticsearch.ExceptionsHelper;
 import org.elasticsearch.ResourceNotFoundException;
@@ -1405,7 +1404,7 @@ public class SearchableSnapshotsIntegTests extends BaseSearchableSnapshotsIntegT
         final long totalSize = statsResponse.getStats()
             .stream()
             .flatMap(s -> s.getStats().stream())
-            .mapToLong(SearchableSnapshotShardStats.CacheIndexInputStats::getFileLength)
+            .mapToLong(SearchableSnapshotShardStats.CacheIndexInputStats::getTotalSize)
             .sum();
         final Set<String> nodeIdsWithLargeEnoughCache = new HashSet<>();
         for (ObjectCursor<DiscoveryNode> nodeCursor : client().admin()
@@ -1430,37 +1429,37 @@ public class SearchableSnapshotsIntegTests extends BaseSearchableSnapshotsIntegT
             assertThat(stats.getShardRouting().getIndexName(), equalTo(indexName));
             if (shardRouting.started()) {
                 for (SearchableSnapshotShardStats.CacheIndexInputStats indexInputStats : stats.getStats()) {
-                    final String fileName = indexInputStats.getFileName();
+                    final String fileExt = indexInputStats.getFileExt();
                     assertThat(
-                        "Unexpected open count for " + fileName + " of shard " + shardRouting,
+                        "Unexpected open count for " + fileExt + " of shard " + shardRouting,
                         indexInputStats.getOpenCount(),
                         greaterThan(0L)
                     );
                     assertThat(
-                        "Unexpected close count for " + fileName + " of shard " + shardRouting,
+                        "Unexpected close count for " + fileExt + " of shard " + shardRouting,
                         indexInputStats.getCloseCount(),
                         lessThanOrEqualTo(indexInputStats.getOpenCount())
                     );
                     assertThat(
-                        "Unexpected file length for " + fileName + " of shard " + shardRouting,
-                        indexInputStats.getFileLength(),
+                        "Unexpected file length for " + fileExt + " of shard " + shardRouting,
+                        indexInputStats.getTotalSize(),
                         greaterThan(0L)
                     );
 
-                    if (cacheEnabled == false || nonCachedExtensions.contains(IndexFileNames.getExtension(fileName))) {
+                    if (cacheEnabled == false || nonCachedExtensions.contains(fileExt)) {
                         assertThat(
-                            "Expected at least 1 optimized or direct read for " + fileName + " of shard " + shardRouting,
+                            "Expected at least 1 optimized or direct read for " + fileExt + " of shard " + shardRouting,
                             max(indexInputStats.getOptimizedBytesRead().getCount(), indexInputStats.getDirectBytesRead().getCount()),
                             greaterThan(0L)
                         );
                         assertThat(
-                            "Expected no cache read or write for " + fileName + " of shard " + shardRouting,
+                            "Expected no cache read or write for " + fileExt + " of shard " + shardRouting,
                             max(indexInputStats.getCachedBytesRead().getCount(), indexInputStats.getCachedBytesWritten().getCount()),
                             equalTo(0L)
                         );
                     } else if (nodeIdsWithLargeEnoughCache.contains(stats.getShardRouting().currentNodeId())) {
                         assertThat(
-                            "Expected at least 1 cache read or write for " + fileName + " of shard " + shardRouting,
+                            "Expected at least 1 cache read or write for " + fileExt + " of shard " + shardRouting,
                             max(
                                 indexInputStats.getCachedBytesRead().getCount(),
                                 indexInputStats.getCachedBytesWritten().getCount(),
@@ -1469,18 +1468,18 @@ public class SearchableSnapshotsIntegTests extends BaseSearchableSnapshotsIntegT
                             greaterThan(0L)
                         );
                         assertThat(
-                            "Expected no optimized read for " + fileName + " of shard " + shardRouting,
+                            "Expected no optimized read for " + fileExt + " of shard " + shardRouting,
                             indexInputStats.getOptimizedBytesRead().getCount(),
                             equalTo(0L)
                         );
                         assertThat(
-                            "Expected no direct read for " + fileName + " of shard " + shardRouting,
+                            "Expected no direct read for " + fileExt + " of shard " + shardRouting,
                             indexInputStats.getDirectBytesRead().getCount(),
                             equalTo(0L)
                         );
                     } else {
                         assertThat(
-                            "Expected at least 1 read or write of any kind for " + fileName + " of shard " + shardRouting,
+                            "Expected at least 1 read or write of any kind for " + fileExt + " of shard " + shardRouting,
                             max(
                                 indexInputStats.getCachedBytesRead().getCount(),
                                 indexInputStats.getCachedBytesWritten().getCount(),

+ 15 - 9
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/index/store/IndexInputStats.java

@@ -25,7 +25,8 @@ public class IndexInputStats {
     /* A threshold beyond which an index input seeking is counted as "large" */
     static final ByteSizeValue SEEKING_THRESHOLD = new ByteSizeValue(8, ByteSizeUnit.MB);
 
-    private final long fileLength;
+    private final int numFiles;
+    private final long totalSize;
     private final long seekingThreshold;
     private final LongSupplier currentTimeNanos;
 
@@ -51,12 +52,13 @@ public class IndexInputStats {
     private final Counter blobStoreBytesRequested = new Counter();
     private final AtomicLong currentIndexCacheFills = new AtomicLong();
 
-    public IndexInputStats(long fileLength, LongSupplier currentTimeNanos) {
-        this(fileLength, SEEKING_THRESHOLD.getBytes(), currentTimeNanos);
+    public IndexInputStats(int numFiles, long totalSize, LongSupplier currentTimeNanos) {
+        this(numFiles, totalSize, SEEKING_THRESHOLD.getBytes(), currentTimeNanos);
     }
 
-    public IndexInputStats(long fileLength, long seekingThreshold, LongSupplier currentTimeNanos) {
-        this.fileLength = fileLength;
+    public IndexInputStats(int numFiles, long totalSize, long seekingThreshold, LongSupplier currentTimeNanos) {
+        this.numFiles = numFiles;
+        this.totalSize = totalSize;
         this.seekingThreshold = seekingThreshold;
         this.currentTimeNanos = currentTimeNanos;
     }
@@ -115,9 +117,9 @@ public class IndexInputStats {
             }
         } else {
             if (isLarge) {
-                backwardLargeSeeks.add(delta);
+                backwardLargeSeeks.add(-delta);
             } else {
-                backwardSmallSeeks.add(delta);
+                backwardSmallSeeks.add(-delta);
             }
         }
     }
@@ -135,8 +137,12 @@ public class IndexInputStats {
         };
     }
 
-    public long getFileLength() {
-        return fileLength;
+    public int getNumFiles() {
+        return numFiles;
+    }
+
+    public long getTotalSize() {
+        return totalSize;
     }
 
     public LongAdder getOpened() {

+ 20 - 5
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/index/store/SearchableSnapshotDirectory.java

@@ -275,9 +275,10 @@ public class SearchableSnapshotDirectory extends BaseDirectory {
         return Collections.unmodifiableMap(stats);
     }
 
+    // only used for tests
     @Nullable
-    public IndexInputStats getStats(String fileName) {
-        return stats.get(fileName);
+    IndexInputStats getStats(String fileName) {
+        return stats.get(getNonNullFileExt(fileName));
     }
 
     private BlobStoreIndexShardSnapshot.FileInfo fileInfo(final String name) throws FileNotFoundException {
@@ -354,8 +355,8 @@ public class SearchableSnapshotDirectory extends BaseDirectory {
         }
     }
 
-    protected IndexInputStats createIndexInputStats(final long fileLength) {
-        return new IndexInputStats(fileLength, statsCurrentTimeNanosSupplier);
+    protected IndexInputStats createIndexInputStats(final int numFiles, final long totalSize) {
+        return new IndexInputStats(numFiles, totalSize, statsCurrentTimeNanosSupplier);
     }
 
     public CacheKey createCacheKey(String fileName) {
@@ -387,7 +388,16 @@ public class SearchableSnapshotDirectory extends BaseDirectory {
             return ChecksumBlobContainerIndexInput.create(fileInfo.physicalName(), fileInfo.length(), fileInfo.checksum(), context);
         }
 
-        final IndexInputStats inputStats = stats.computeIfAbsent(name, n -> createIndexInputStats(fileInfo.length()));
+        final String ext = getNonNullFileExt(name);
+        final IndexInputStats inputStats = stats.computeIfAbsent(ext, n -> {
+            // get all fileInfo with same extension
+            final Tuple<Integer, Long> fileExtCompoundStats = files().stream()
+                .filter(fi -> ext.equals(getNonNullFileExt(fi.physicalName())))
+                .map(fi -> Tuple.tuple(1, fi.length()))
+                .reduce((t1, t2) -> Tuple.tuple(t1.v1() + t2.v1(), t1.v2() + t2.v2()))
+                .get();
+            return createIndexInputStats(fileExtCompoundStats.v1(), fileExtCompoundStats.v2());
+        });
         if (useCache && isExcludedFromCache(name) == false) {
             if (partial) {
                 return new FrozenIndexInput(
@@ -420,6 +430,11 @@ public class SearchableSnapshotDirectory extends BaseDirectory {
         }
     }
 
+    static String getNonNullFileExt(String name) {
+        final String ext = IndexFileNames.getExtension(name);
+        return ext == null ? "" : ext;
+    }
+
     private long getUncachedChunkSize() {
         if (uncachedChunkSize < 0) {
             return blobContainer().readBlobPreferredLength();

+ 84 - 27
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponse.java

@@ -14,18 +14,23 @@ import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.index.Index;
 import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats;
+import org.elasticsearch.xpack.core.searchablesnapshots.SearchableSnapshotShardStats.CacheIndexInputStats;
 
 import java.io.IOException;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
+import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
 
 public class SearchableSnapshotsStatsResponse extends BroadcastResponse {
 
     private List<SearchableSnapshotShardStats> stats;
+    private volatile List<CacheIndexInputStats> total;
 
     SearchableSnapshotsStatsResponse(StreamInput in) throws IOException {
         super(in);
@@ -47,6 +52,22 @@ public class SearchableSnapshotsStatsResponse extends BroadcastResponse {
         return stats;
     }
 
+    private List<CacheIndexInputStats> getTotal() {
+        if (total != null) {
+            return total;
+        }
+        return total = computeCompound(getStats().stream().flatMap(stat -> stat.getStats().stream()));
+    }
+
+    private static List<CacheIndexInputStats> computeCompound(Stream<CacheIndexInputStats> statsStream) {
+        return statsStream.collect(groupingBy(CacheIndexInputStats::getFileExt, Collectors.reducing(CacheIndexInputStats::combine)))
+            .values()
+            .stream()
+            .filter(o -> o.isEmpty() == false)
+            .map(Optional::get)
+            .collect(toList());
+    }
+
     @Override
     public void writeTo(StreamOutput out) throws IOException {
         super.writeTo(out);
@@ -55,42 +76,78 @@ public class SearchableSnapshotsStatsResponse extends BroadcastResponse {
 
     @Override
     protected void addCustomXContentFields(XContentBuilder builder, Params params) throws IOException {
-        final List<Index> indices = getStats().stream()
-            .filter(stats -> stats.getStats().isEmpty() == false)
-            .map(SearchableSnapshotShardStats::getShardRouting)
-            .map(ShardRouting::index)
-            .sorted(Comparator.comparing(Index::getName))
-            .distinct()
-            .collect(toList());
+        final String level = params.param("level", "indices");
+        final boolean isLevelValid = "cluster".equalsIgnoreCase(level)
+            || "indices".equalsIgnoreCase(level)
+            || "shards".equalsIgnoreCase(level);
+        if (isLevelValid == false) {
+            throw new IllegalArgumentException("level parameter must be one of [cluster] or [indices] or [shards] but was [" + level + "]");
+        }
 
-        builder.startObject("indices");
-        for (Index index : indices) {
-            builder.startObject(index.getName());
-            {
-                builder.startObject("shards");
+        builder.startArray("total");
+        for (CacheIndexInputStats cis : getTotal()) {
+            cis.toXContent(builder, params);
+        }
+        builder.endArray();
+
+        if ("indices".equalsIgnoreCase(level) || "shards".equalsIgnoreCase(level)) {
+            builder.startObject("indices");
+            final List<Index> indices = getStats().stream()
+                .filter(stats -> stats.getStats().isEmpty() == false)
+                .map(SearchableSnapshotShardStats::getShardRouting)
+                .map(ShardRouting::index)
+                .sorted(Comparator.comparing(Index::getName))
+                .distinct()
+                .collect(toList());
+
+            for (Index index : indices) {
+                builder.startObject(index.getName());
                 {
-                    List<SearchableSnapshotShardStats> listOfStats = getStats().stream()
-                        .filter(dirStats -> dirStats.getShardRouting().index().equals(index))
-                        .sorted(Comparator.comparingInt(dir -> dir.getShardRouting().getId()))
-                        .collect(Collectors.toList());
-
-                    int minShard = listOfStats.stream().map(stat -> stat.getShardRouting().getId()).min(Integer::compareTo).orElse(0);
-                    int maxShard = listOfStats.stream().map(stat -> stat.getShardRouting().getId()).max(Integer::compareTo).orElse(0);
-
-                    for (int i = minShard; i <= maxShard; i++) {
-                        builder.startArray(Integer.toString(i));
-                        for (SearchableSnapshotShardStats stat : listOfStats) {
-                            if (stat.getShardRouting().getId() == i) {
-                                stat.toXContent(builder, params);
+                    builder.startArray("total");
+                    List<CacheIndexInputStats> indexStats = computeCompound(
+                        getStats().stream()
+                            .filter(dirStats -> dirStats.getShardRouting().index().equals(index))
+                            .flatMap(dirStats -> dirStats.getStats().stream())
+                    );
+
+                    for (CacheIndexInputStats cis : indexStats) {
+                        cis.toXContent(builder, params);
+                    }
+                    builder.endArray();
+
+                    if ("shards".equalsIgnoreCase(level)) {
+                        builder.startObject("shards");
+                        {
+                            List<SearchableSnapshotShardStats> listOfStats = getStats().stream()
+                                .filter(dirStats -> dirStats.getShardRouting().index().equals(index))
+                                .sorted(Comparator.comparingInt(dir -> dir.getShardRouting().getId()))
+                                .collect(Collectors.toList());
+
+                            int minShard = listOfStats.stream()
+                                .map(stat -> stat.getShardRouting().getId())
+                                .min(Integer::compareTo)
+                                .orElse(0);
+                            int maxShard = listOfStats.stream()
+                                .map(stat -> stat.getShardRouting().getId())
+                                .max(Integer::compareTo)
+                                .orElse(0);
+
+                            for (int i = minShard; i <= maxShard; i++) {
+                                builder.startArray(Integer.toString(i));
+                                for (SearchableSnapshotShardStats stat : listOfStats) {
+                                    if (stat.getShardRouting().getId() == i) {
+                                        stat.toXContent(builder, params);
+                                    }
+                                }
+                                builder.endArray();
                             }
                         }
-                        builder.endArray();
+                        builder.endObject();
                     }
                 }
                 builder.endObject();
             }
             builder.endObject();
         }
-        builder.endObject();
     }
 }

+ 4 - 3
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/action/TransportSearchableSnapshotsStatsAction.java

@@ -96,10 +96,11 @@ public class TransportSearchableSnapshotsStatsAction extends AbstractTransportSe
         );
     }
 
-    private static CacheIndexInputStats toCacheIndexInputStats(final String fileName, final IndexInputStats inputStats) {
+    private static CacheIndexInputStats toCacheIndexInputStats(final String fileExt, final IndexInputStats inputStats) {
         return new CacheIndexInputStats(
-            fileName,
-            inputStats.getFileLength(),
+            fileExt,
+            inputStats.getNumFiles(),
+            inputStats.getTotalSize(),
             inputStats.getOpened().sum(),
             inputStats.getClosed().sum(),
             toCounter(inputStats.getForwardSmallSeeks()),

+ 9 - 0
x-pack/plugin/searchable-snapshots/src/main/java/org/elasticsearch/xpack/searchablesnapshots/rest/RestSearchableSnapshotsStatsAction.java

@@ -14,7 +14,9 @@ import org.elasticsearch.rest.action.RestToXContentListener;
 import org.elasticsearch.xpack.searchablesnapshots.action.SearchableSnapshotsStatsAction;
 import org.elasticsearch.xpack.searchablesnapshots.action.SearchableSnapshotsStatsRequest;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 public class RestSearchableSnapshotsStatsAction extends BaseRestHandler {
 
@@ -40,4 +42,11 @@ public class RestSearchableSnapshotsStatsAction extends BaseRestHandler {
             new RestToXContentListener<>(channel)
         );
     }
+
+    private static final Set<String> RESPONSE_PARAMS = Collections.singleton("level");
+
+    @Override
+    protected Set<String> responseParams() {
+        return RESPONSE_PARAMS;
+    }
 }

+ 8 - 8
x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/index/store/IndexInputStatsTests.java

@@ -23,7 +23,7 @@ public class IndexInputStatsTests extends ESTestCase {
 
     public void testReads() {
         final long fileLength = randomLongBetween(1L, 1_000L);
-        final IndexInputStats inputStats = new IndexInputStats(fileLength, FAKE_CLOCK);
+        final IndexInputStats inputStats = new IndexInputStats(1, fileLength, FAKE_CLOCK);
 
         assertCounter(inputStats.getContiguousReads(), 0L, 0L, 0L, 0L);
         assertCounter(inputStats.getNonContiguousReads(), 0L, 0L, 0L, 0L);
@@ -32,9 +32,9 @@ public class IndexInputStatsTests extends ESTestCase {
         final IndexInputStats.Counter nonContiguous = new IndexInputStats.Counter();
 
         for (int i = 0; i < randomIntBetween(1, 50); i++) {
-            final long currentPosition = randomLongBetween(0L, inputStats.getFileLength() - 1L);
-            final long previousPosition = randomBoolean() ? currentPosition : randomLongBetween(0L, inputStats.getFileLength() - 1L);
-            final int bytesRead = randomIntBetween(1, toIntBytes(Math.max(1L, inputStats.getFileLength() - currentPosition)));
+            final long currentPosition = randomLongBetween(0L, inputStats.getTotalSize() - 1L);
+            final long previousPosition = randomBoolean() ? currentPosition : randomLongBetween(0L, inputStats.getTotalSize() - 1L);
+            final int bytesRead = randomIntBetween(1, toIntBytes(Math.max(1L, inputStats.getTotalSize() - currentPosition)));
 
             inputStats.incrementBytesRead(previousPosition, currentPosition, bytesRead);
 
@@ -58,7 +58,7 @@ public class IndexInputStatsTests extends ESTestCase {
     public void testSeeks() {
         final long fileLength = randomLongBetween(1L, 1_000L);
         final long seekingThreshold = randomBoolean() ? randomLongBetween(1L, fileLength) : SEEKING_THRESHOLD.getBytes();
-        final IndexInputStats inputStats = new IndexInputStats(fileLength, seekingThreshold, FAKE_CLOCK);
+        final IndexInputStats inputStats = new IndexInputStats(1, fileLength, seekingThreshold, FAKE_CLOCK);
 
         assertCounter(inputStats.getForwardSmallSeeks(), 0L, 0L, 0L, 0L);
         assertCounter(inputStats.getForwardLargeSeeks(), 0L, 0L, 0L, 0L);
@@ -81,7 +81,7 @@ public class IndexInputStatsTests extends ESTestCase {
                 forwardCounter.add(delta);
             } else if (delta < 0) {
                 IndexInputStats.Counter backwardCounter = (delta >= -1 * seekingThreshold) ? bwSmallSeeks : bwLargeSeeks;
-                backwardCounter.add(delta);
+                backwardCounter.add(-delta);
             }
         }
 
@@ -117,8 +117,8 @@ public class IndexInputStatsTests extends ESTestCase {
     }
 
     public void testSeekToSamePosition() {
-        final IndexInputStats inputStats = new IndexInputStats(randomLongBetween(1L, 1_000L), FAKE_CLOCK);
-        final long position = randomLongBetween(0L, inputStats.getFileLength());
+        final IndexInputStats inputStats = new IndexInputStats(1, randomLongBetween(1L, 1_000L), FAKE_CLOCK);
+        final long position = randomLongBetween(0L, inputStats.getTotalSize());
 
         inputStats.incrementSeeks(position, position);
 

+ 10 - 10
x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/index/store/SearchableSnapshotDirectoryStatsTests.java

@@ -509,20 +509,20 @@ public class SearchableSnapshotDirectoryStatsTests extends AbstractSearchableSna
 
                 do {
                     long moveBackward = -1L * randomLongBetween(1L, input.getFilePointer());
+                    long moveBackwardAbs = Math.abs(moveBackward);
                     input.seek(input.getFilePointer() + moveBackward);
 
                     if (inputStats.isLargeSeek(moveBackward)) {
-                        minLargeSeeks = (moveBackward < minLargeSeeks) ? moveBackward : minLargeSeeks;
-                        maxLargeSeeks = (moveBackward > maxLargeSeeks) ? moveBackward : maxLargeSeeks;
-                        totalLargeSeeks += moveBackward;
+                        minLargeSeeks = (moveBackwardAbs < minLargeSeeks) ? moveBackwardAbs : minLargeSeeks;
+                        maxLargeSeeks = (moveBackwardAbs > maxLargeSeeks) ? moveBackwardAbs : maxLargeSeeks;
+                        totalLargeSeeks += moveBackwardAbs;
                         countLargeSeeks += 1;
 
                         assertCounter(backwardLargeSeeks, totalLargeSeeks, countLargeSeeks, minLargeSeeks, maxLargeSeeks);
-
                     } else {
-                        minSmallSeeks = (moveBackward < minSmallSeeks) ? moveBackward : minSmallSeeks;
-                        maxSmallSeeks = (moveBackward > maxSmallSeeks) ? moveBackward : maxSmallSeeks;
-                        totalSmallSeeks += moveBackward;
+                        minSmallSeeks = (moveBackwardAbs < minSmallSeeks) ? moveBackwardAbs : minSmallSeeks;
+                        maxSmallSeeks = (moveBackwardAbs > maxSmallSeeks) ? moveBackwardAbs : maxSmallSeeks;
+                        totalSmallSeeks += moveBackwardAbs;
                         countSmallSeeks += 1;
 
                         assertCounter(backwardSmallSeeks, totalSmallSeeks, countSmallSeeks, minSmallSeeks, maxSmallSeeks);
@@ -644,11 +644,11 @@ public class SearchableSnapshotDirectoryStatsTests extends AbstractSearchableSna
                 frozenCacheService
             ) {
                 @Override
-                protected IndexInputStats createIndexInputStats(long fileLength) {
+                protected IndexInputStats createIndexInputStats(final int numFiles, final long totalSize) {
                     if (seekingThreshold == null) {
-                        return super.createIndexInputStats(fileLength);
+                        return super.createIndexInputStats(numFiles, totalSize);
                     }
-                    return new IndexInputStats(fileLength, seekingThreshold, statsCurrentTimeNanos);
+                    return new IndexInputStats(numFiles, totalSize, seekingThreshold, statsCurrentTimeNanos);
                 }
             }
         ) {

+ 1 - 1
x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/index/store/direct/DirectBlobContainerIndexInputTests.java

@@ -113,7 +113,7 @@ public class DirectBlobContainerIndexInputTests extends ESIndexInputTestCase {
             blobContainer,
             fileInfo,
             newIOContext(random()),
-            new IndexInputStats(0L, () -> 0L),
+            new IndexInputStats(1, 0L, () -> 0L),
             minimumReadSize,
             randomBoolean() ? BufferedIndexInput.BUFFER_SIZE : between(BufferedIndexInput.MIN_BUFFER_SIZE, BufferedIndexInput.BUFFER_SIZE)
         );

+ 3 - 1
x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/AbstractSearchableSnapshotsRestTestCase.java

@@ -429,7 +429,9 @@ public abstract class AbstractSearchableSnapshotsRestTestCase extends ESRestTest
     }
 
     protected static Map<String, Object> searchableSnapshotStats(String index) throws IOException {
-        final Response response = client().performRequest(new Request(HttpGet.METHOD_NAME, '/' + index + "/_searchable_snapshots/stats"));
+        final Request request = new Request(HttpGet.METHOD_NAME, '/' + index + "/_searchable_snapshots/stats");
+        request.addParameter("level", "shards");
+        final Response response = client().performRequest(request);
         assertThat(
             "Failed to retrieve searchable snapshots stats for on index [" + index + "]: " + response,
             response.getStatusLine().getStatusCode(),

+ 1 - 0
x-pack/plugin/searchable-snapshots/src/test/java/org/elasticsearch/xpack/searchablesnapshots/action/SearchableSnapshotsStatsResponseTests.java

@@ -101,6 +101,7 @@ public class SearchableSnapshotsStatsResponseTests extends ESTestCase {
             randomNonNegativeLong(),
             randomNonNegativeLong(),
             randomNonNegativeLong(),
+            randomNonNegativeLong(),
             randomCounter(),
             randomCounter(),
             randomCounter(),

+ 12 - 0
x-pack/plugin/src/test/resources/rest-api-spec/api/searchable_snapshots.stats.json

@@ -30,6 +30,18 @@
           }
         }
       ]
+    },
+    "params": {
+      "level":{
+        "type":"enum",
+        "description":"Return stats aggregated at cluster, index or shard level",
+        "options":[
+          "cluster",
+          "indices",
+          "shards"
+        ],
+        "default":"indices"
+      }
     }
   }
 }