Browse Source

Add index creation version stats to cluster stats (#68141)

This commit adds statistics about the index creation versions to the `/_cluster/stats` endpoint. The
stats look like:

```
{
  "_nodes" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "indices" : {
    "count" : 3,
    ...
    "versions" : [
      {
        "version" : "8.0.0",
        "index_count" : 1,
        "primary_shard_count" : 2,
        "total_primary_size" : "8.6kb",
        "total_primary_bytes" : 8831
      },
      {
        "version" : "7.11.0",
        "index_count" : 1,
        "primary_shard_count" : 1,
        "total_primary_size" : "4.6kb",
        "total_primary_bytes" : 4230
      }
    ]
  },
  ...
}
```

(`total_primary_size` is only shown with the `?human` flag)

This is useful for telemetry as it allows us to see if/when a cluster has indices created on a
previous version that would need to be either upgraded or supported during an upgrade.
Lee Hinman 4 years ago
parent
commit
ac1433d300

+ 10 - 1
docs/reference/cluster/stats.asciidoc

@@ -1214,7 +1214,16 @@ The API returns the following response:
         "built_in_tokenizers": [],
         "built_in_filters": [],
         "built_in_analyzers": []
-      }
+      },
+      "versions": [
+        {
+          "version": "8.0.0",
+          "index_count": 1,
+          "primary_shard_count": 1,
+          "total_primary_size": "7.4kb",
+          "total_primary_bytes": 7632
+        }
+      ]
    },
    "nodes": {
       "count": {

+ 13 - 5
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsIndices.java

@@ -45,12 +45,12 @@ public class ClusterStatsIndices implements ToXContentFragment {
     private QueryCacheStats queryCache;
     private CompletionStats completion;
     private SegmentsStats segments;
-    private AnalysisStats analysis;
-    private MappingStats mappings;
+    private final AnalysisStats analysis;
+    private final MappingStats mappings;
+    private final VersionStats versions;
 
-    public ClusterStatsIndices(List<ClusterStatsNodeResponse> nodeResponses,
-            MappingStats mappingStats,
-            AnalysisStats analysisStats) {
+    public ClusterStatsIndices(List<ClusterStatsNodeResponse> nodeResponses, MappingStats mappingStats,
+                               AnalysisStats analysisStats, VersionStats versionStats) {
         ObjectObjectHashMap<String, ShardStats> countsPerIndex = new ObjectObjectHashMap<>();
 
         this.docs = new DocsStats();
@@ -92,6 +92,7 @@ public class ClusterStatsIndices implements ToXContentFragment {
 
         this.mappings = mappingStats;
         this.analysis = analysisStats;
+        this.versions = versionStats;
     }
 
     public int getIndexCount() {
@@ -134,6 +135,10 @@ public class ClusterStatsIndices implements ToXContentFragment {
         return analysis;
     }
 
+    public VersionStats getVersions() {
+        return versions;
+    }
+
     static final class Fields {
         static final String COUNT = "count";
     }
@@ -154,6 +159,9 @@ public class ClusterStatsIndices implements ToXContentFragment {
         if (analysis != null) {
             analysis.toXContent(builder, params);
         }
+        if (versions != null) {
+            versions.toXContent(builder, params);
+        }
         return builder;
     }
 

+ 12 - 3
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/ClusterStatsResponse.java

@@ -19,6 +19,7 @@
 
 package org.elasticsearch.action.admin.cluster.stats;
 
+import org.elasticsearch.Version;
 import org.elasticsearch.action.FailedNodeException;
 import org.elasticsearch.action.support.nodes.BaseNodesResponse;
 import org.elasticsearch.cluster.ClusterName;
@@ -51,11 +52,15 @@ public class ClusterStatsResponse extends BaseNodesResponse<ClusterStatsNodeResp
         String clusterUUID = in.readOptionalString();
         MappingStats mappingStats = in.readOptionalWriteable(MappingStats::new);
         AnalysisStats analysisStats = in.readOptionalWriteable(AnalysisStats::new);
+        VersionStats versionStats = null;
+        if (in.getVersion().onOrAfter(Version.V_8_0_0)) {
+            versionStats = in.readOptionalWriteable(VersionStats::new);
+        }
         this.clusterUUID = clusterUUID;
 
         // built from nodes rather than from the stream directly
         nodesStats = new ClusterStatsNodes(getNodes());
-        indicesStats = new ClusterStatsIndices(getNodes(), mappingStats, analysisStats);
+        indicesStats = new ClusterStatsIndices(getNodes(), mappingStats, analysisStats, versionStats);
     }
 
     public ClusterStatsResponse(long timestamp,
@@ -64,12 +69,13 @@ public class ClusterStatsResponse extends BaseNodesResponse<ClusterStatsNodeResp
                                 List<ClusterStatsNodeResponse> nodes,
                                 List<FailedNodeException> failures,
                                 MappingStats mappingStats,
-                                AnalysisStats analysisStats) {
+                                AnalysisStats analysisStats,
+                                VersionStats versionStats) {
         super(clusterName, nodes, failures);
         this.clusterUUID = clusterUUID;
         this.timestamp = timestamp;
         nodesStats = new ClusterStatsNodes(nodes);
-        indicesStats = new ClusterStatsIndices(nodes, mappingStats, analysisStats);
+        indicesStats = new ClusterStatsIndices(nodes, mappingStats, analysisStats, versionStats);
         ClusterHealthStatus status = null;
         for (ClusterStatsNodeResponse response : nodes) {
             // only the master node populates the status
@@ -109,6 +115,9 @@ public class ClusterStatsResponse extends BaseNodesResponse<ClusterStatsNodeResp
         out.writeOptionalString(clusterUUID);
         out.writeOptionalWriteable(indicesStats.getMappings());
         out.writeOptionalWriteable(indicesStats.getAnalysis());
+        if (out.getVersion().onOrAfter(Version.V_8_0_0)) {
+            out.writeOptionalWriteable(indicesStats.getVersions());
+        }
     }
 
     @Override

+ 3 - 1
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/TransportClusterStatsAction.java

@@ -117,6 +117,7 @@ public class TransportClusterStatsAction extends TransportNodesAction<ClusterSta
                 }
             }
         }
+        VersionStats versionStats = VersionStats.of(metadata, responses);
         return new ClusterStatsResponse(
             System.currentTimeMillis(),
             state.metadata().clusterUUID(),
@@ -124,7 +125,8 @@ public class TransportClusterStatsAction extends TransportNodesAction<ClusterSta
             responses,
             failures,
             currentMappingStats,
-            currentAnalysisStats);
+            currentAnalysisStats,
+            versionStats);
     }
 
     @Override

+ 250 - 0
server/src/main/java/org/elasticsearch/action/admin/cluster/stats/VersionStats.java

@@ -0,0 +1,250 @@
+/*
+ * 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.action.admin.cluster.stats;
+
+import com.carrotsearch.hppc.cursors.ObjectObjectCursor;
+import org.elasticsearch.Version;
+import org.elasticsearch.action.admin.indices.stats.ShardStats;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.unit.ByteSizeValue;
+import org.elasticsearch.common.xcontent.ToXContentFragment;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * {@link VersionStats} calculates statistics for index creation versions mapped to the number of
+ * indices, primary shards, and size of primary shards on disk. This is used from
+ * {@link ClusterStatsIndices} and exposed as part of the {@code "/_cluster/stats"} API.
+ */
+public final class VersionStats implements ToXContentFragment, Writeable {
+
+    private final Set<SingleVersionStats> versionStats;
+
+    public static VersionStats of(Metadata metadata, List<ClusterStatsNodeResponse> nodeResponses) {
+        final Map<Version, Integer> indexCounts = new HashMap<>();
+        final Map<Version, Integer> primaryShardCounts = new HashMap<>();
+        final Map<Version, Long> primaryByteCounts = new HashMap<>();
+        final Map<String, List<ShardStats>> indexPrimaryShardStats = new HashMap<>();
+
+        // Build a map from index name to primary shard stats
+        for (ClusterStatsNodeResponse r : nodeResponses) {
+            for (ShardStats shardStats : r.shardsStats()) {
+                if (shardStats.getShardRouting().primary()) {
+                    indexPrimaryShardStats.compute(shardStats.getShardRouting().getIndexName(), (name, stats) -> {
+                        if (stats == null) {
+                            List<ShardStats> newStats = new ArrayList<>();
+                            newStats.add(shardStats);
+                            return newStats;
+                        } else {
+                            stats.add(shardStats);
+                            return stats;
+                        }
+                    });
+                }
+            }
+        }
+
+        // Loop through all indices in the metadata, building the counts as needed
+        for (ObjectObjectCursor<String, IndexMetadata> cursor : metadata.indices()) {
+            IndexMetadata indexMetadata = cursor.value;
+            // Increment version-specific index counts
+            indexCounts.compute(indexMetadata.getCreationVersion(), (v, i) -> {
+                if (i == null) {
+                    return 1;
+                } else {
+                    return i + 1;
+                }
+            });
+            // Increment version-specific primary shard counts
+            primaryShardCounts.compute(indexMetadata.getCreationVersion(), (v, i) -> {
+                if (i == null) {
+                    return indexMetadata.getNumberOfShards();
+                } else {
+                    return i + indexMetadata.getNumberOfShards();
+                }
+            });
+            // Increment version-specific primary shard sizes
+            primaryByteCounts.compute(indexMetadata.getCreationVersion(), (v, i) -> {
+                String indexName = indexMetadata.getIndex().getName();
+                long indexPrimarySize = indexPrimaryShardStats.getOrDefault(indexName, Collections.emptyList()).stream()
+                    .mapToLong(stats -> stats.getStats().getStore().sizeInBytes())
+                    .sum();
+                if (i == null) {
+                    return indexPrimarySize;
+                } else {
+                    return i + indexPrimarySize;
+                }
+            });
+        }
+        List<SingleVersionStats> calculatedStats = new ArrayList<>(indexCounts.size());
+        for (Map.Entry<Version, Integer> indexVersionCount : indexCounts.entrySet()) {
+            Version v = indexVersionCount.getKey();
+            SingleVersionStats singleStats = new SingleVersionStats(v, indexVersionCount.getValue(),
+                primaryShardCounts.getOrDefault(v, 0), primaryByteCounts.getOrDefault(v, 0L));
+            calculatedStats.add(singleStats);
+        }
+        return new VersionStats(calculatedStats);
+    }
+
+    VersionStats(Collection<SingleVersionStats> versionStats) {
+        this.versionStats = Collections.unmodifiableSet(new TreeSet<>(versionStats));
+    }
+
+    VersionStats(StreamInput in) throws IOException {
+        this.versionStats = Collections.unmodifiableSet(new TreeSet<>(in.readList(SingleVersionStats::new)));
+    }
+
+    public Set<SingleVersionStats> versionStats() {
+        return this.versionStats;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startArray("versions");
+        for (SingleVersionStats stat : versionStats) {
+            stat.toXContent(builder, params);
+        }
+        builder.endArray();
+        return builder;
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeCollection(versionStats);
+    }
+
+    @Override
+    public int hashCode() {
+        return versionStats.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        VersionStats other = (VersionStats) obj;
+        return versionStats.equals(other.versionStats);
+    }
+
+    @Override
+    public String toString() {
+        return Strings.toString(this);
+    }
+
+    static class SingleVersionStats implements ToXContentObject, Writeable, Comparable<SingleVersionStats> {
+
+        public final Version version;
+        public final int indexCount;
+        public final int primaryShardCount;
+        public final long totalPrimaryByteCount;
+
+        SingleVersionStats(Version version, int indexCount, int primaryShardCount, long totalPrimaryByteCount) {
+            this.version = version;
+            this.indexCount = indexCount;
+            this.primaryShardCount = primaryShardCount;
+            this.totalPrimaryByteCount = totalPrimaryByteCount;
+        }
+
+        SingleVersionStats(StreamInput in) throws IOException {
+            this.version = Version.readVersion(in);
+            this.indexCount = in.readVInt();
+            this.primaryShardCount = in.readVInt();
+            this.totalPrimaryByteCount = in.readVLong();
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field("version", version.toString());
+            builder.field("index_count", indexCount);
+            builder.field("primary_shard_count", primaryShardCount);
+            builder.humanReadableField("total_primary_bytes", "total_primary_size", new ByteSizeValue(totalPrimaryByteCount));
+            builder.endObject();
+            return builder;
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            Version.writeVersion(this.version, out);
+            out.writeVInt(this.indexCount);
+            out.writeVInt(this.primaryShardCount);
+            out.writeVLong(this.totalPrimaryByteCount);
+        }
+
+        @Override
+        public int compareTo(SingleVersionStats o) {
+            if (this.equals(o)) {
+                return 0;
+            }
+            if (this.version.equals(o.version)) {
+                // never 0, this is to make the comparator consistent with equals
+                return -1;
+            }
+            return this.version.compareTo(o.version);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(version, indexCount, primaryShardCount, totalPrimaryByteCount);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+
+            SingleVersionStats other = (SingleVersionStats) obj;
+            return version.equals(other.version) &&
+                indexCount == other.indexCount &&
+                primaryShardCount == other.primaryShardCount &&
+                totalPrimaryByteCount == other.totalPrimaryByteCount;
+        }
+
+        @Override
+        public String toString() {
+            return Strings.toString(this);
+        }
+    }
+}

+ 149 - 0
server/src/test/java/org/elasticsearch/action/admin/cluster/stats/VersionStatsTests.java

@@ -0,0 +1,149 @@
+/*
+ * 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.action.admin.cluster.stats;
+
+import org.elasticsearch.Version;
+import org.elasticsearch.action.admin.indices.stats.CommonStats;
+import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
+import org.elasticsearch.action.admin.indices.stats.ShardStats;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.cluster.metadata.IndexMetadata;
+import org.elasticsearch.cluster.metadata.Metadata;
+import org.elasticsearch.cluster.node.DiscoveryNode;
+import org.elasticsearch.cluster.routing.RecoverySource;
+import org.elasticsearch.cluster.routing.ShardRouting;
+import org.elasticsearch.cluster.routing.UnassignedInfo;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.index.shard.IndexShard;
+import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.index.shard.ShardPath;
+import org.elasticsearch.index.store.StoreStats;
+import org.elasticsearch.test.AbstractWireSerializingTestCase;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class VersionStatsTests extends AbstractWireSerializingTestCase<VersionStats> {
+
+    @Override
+    protected Writeable.Reader<VersionStats> instanceReader() {
+        return VersionStats::new;
+    }
+
+    @Override
+    protected VersionStats createTestInstance() {
+        return randomInstance();
+    }
+
+    @Override
+    protected VersionStats mutateInstance(VersionStats instance) throws IOException {
+        return new VersionStats(instance.versionStats().stream()
+            .map(svs -> {
+                switch (randomIntBetween(1, 4)) {
+                    case 1:
+                        return new VersionStats.SingleVersionStats(Version.V_7_3_0,
+                            svs.indexCount, svs.primaryShardCount, svs.totalPrimaryByteCount);
+                    case 2:
+                        return new VersionStats.SingleVersionStats(svs.version,
+                            svs.indexCount + 1, svs.primaryShardCount, svs.totalPrimaryByteCount);
+                    case 3:
+                        return new VersionStats.SingleVersionStats(svs.version,
+                            svs.indexCount, svs.primaryShardCount + 1, svs.totalPrimaryByteCount);
+                    case 4:
+                        return new VersionStats.SingleVersionStats(svs.version,
+                            svs.indexCount, svs.primaryShardCount, svs.totalPrimaryByteCount + 1);
+                    default:
+                        throw new IllegalArgumentException("unexpected branch");
+                }
+            })
+            .collect(Collectors.toList()));
+    }
+
+    public void testCreation() {
+        Metadata metadata = Metadata.builder().build();
+        VersionStats stats = VersionStats.of(metadata, Collections.emptyList());
+        assertThat(stats.versionStats(), equalTo(Collections.emptySet()));
+
+
+        metadata = new Metadata.Builder()
+            .put(indexMeta("foo", Version.CURRENT, 4), true)
+            .put(indexMeta("bar", Version.CURRENT, 3), true)
+            .put(indexMeta("baz", Version.V_7_0_0, 2), true)
+            .build();
+        stats = VersionStats.of(metadata, Collections.emptyList());
+        assertThat(stats.versionStats().size(), equalTo(2));
+        VersionStats.SingleVersionStats s1 = new VersionStats.SingleVersionStats(Version.CURRENT, 2, 7, 0);
+        VersionStats.SingleVersionStats s2 = new VersionStats.SingleVersionStats(Version.V_7_0_0, 1, 2, 0);
+        assertThat(stats.versionStats(), containsInAnyOrder(s1, s2));
+
+        ShardId shardId = new ShardId("bar", "uuid", 0);
+        ShardRouting shardRouting = ShardRouting.newUnassigned(shardId, true,
+            RecoverySource.PeerRecoverySource.INSTANCE, new UnassignedInfo(UnassignedInfo.Reason.INDEX_CREATED, "message"));
+        Path path = createTempDir().resolve("indices").resolve(shardRouting.shardId().getIndex().getUUID())
+            .resolve(String.valueOf(shardRouting.shardId().id()));
+        IndexShard indexShard = mock(IndexShard.class);
+        StoreStats storeStats = new StoreStats(100, 200);
+        when(indexShard.storeStats()).thenReturn(storeStats);
+        ShardStats shardStats = new ShardStats(shardRouting, new ShardPath(false, path, path, shardRouting.shardId()),
+            new CommonStats(null, indexShard, new CommonStatsFlags(CommonStatsFlags.Flag.Store)),
+            null, null, null);
+        ClusterStatsNodeResponse nodeResponse =
+            new ClusterStatsNodeResponse(new DiscoveryNode("id", buildNewFakeTransportAddress(), Version.CURRENT),
+                ClusterHealthStatus.GREEN, null, null, new ShardStats[]{shardStats});
+
+        stats = VersionStats.of(metadata, Collections.singletonList(nodeResponse));
+        assertThat(stats.versionStats().size(), equalTo(2));
+        s1 = new VersionStats.SingleVersionStats(Version.CURRENT, 2, 7, 100);
+        s2 = new VersionStats.SingleVersionStats(Version.V_7_0_0, 1, 2, 0);
+        assertThat(stats.versionStats(), containsInAnyOrder(s1, s2));
+    }
+
+    private static IndexMetadata indexMeta(String name, Version version, int primaryShards) {
+        Settings settings = Settings.builder()
+            .put(IndexMetadata.SETTING_VERSION_CREATED, version)
+            .put(IndexMetadata.SETTING_NUMBER_OF_SHARDS, primaryShards)
+            .put(IndexMetadata.SETTING_NUMBER_OF_REPLICAS, randomIntBetween(0, 3))
+            .build();
+        IndexMetadata.Builder indexMetadata = new IndexMetadata.Builder(name).settings(settings);
+        return indexMetadata.build();
+    }
+
+    public static VersionStats randomInstance() {
+        List<Version> versions = Arrays.asList(Version.CURRENT, Version.V_7_0_0, Version.V_7_1_0, Version.V_7_2_0);
+        List<VersionStats.SingleVersionStats> stats = new ArrayList<>();
+        for (Version v : versions) {
+            VersionStats.SingleVersionStats s =
+                new VersionStats.SingleVersionStats(v, randomIntBetween(10, 20), randomIntBetween(20, 30), randomNonNegativeLong());
+            stats.add(s);
+        }
+        return new VersionStats(stats);
+    }
+}

+ 5 - 2
x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java

@@ -14,6 +14,7 @@ import org.elasticsearch.action.admin.cluster.stats.AnalysisStats;
 import org.elasticsearch.action.admin.cluster.stats.ClusterStatsNodeResponse;
 import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse;
 import org.elasticsearch.action.admin.cluster.stats.MappingStats;
+import org.elasticsearch.action.admin.cluster.stats.VersionStats;
 import org.elasticsearch.action.admin.indices.stats.CommonStats;
 import org.elasticsearch.action.admin.indices.stats.CommonStatsFlags;
 import org.elasticsearch.action.admin.indices.stats.ShardStats;
@@ -334,7 +335,8 @@ public class ClusterStatsMonitoringDocTests extends BaseMonitoringDocTestCase<Cl
                                                                             singletonList(mockNodeResponse),
                                                                             emptyList(),
                                                                             MappingStats.of(metadata),
-                                                                            AnalysisStats.of(metadata));
+                                                                            AnalysisStats.of(metadata),
+                                                                            VersionStats.of(metadata, singletonList(mockNodeResponse)));
 
         final MonitoringDoc.Node node = new MonitoringDoc.Node("_uuid", "_host", "_addr", "_ip", "_name", 1504169190855L);
 
@@ -463,7 +465,8 @@ public class ClusterStatsMonitoringDocTests extends BaseMonitoringDocTestCase<Cl
                 + "        \"built_in_tokenizers\":[],"
                 + "        \"built_in_filters\":[],"
                 + "        \"built_in_analyzers\":[]"
-                + "      }"
+                + "      },"
+                + "      \"versions\":[]"
                 + "    },"
                 + "    \"nodes\": {"
                 + "      \"count\": {"