Browse Source

Add a "verbose" option to the data frame analytics stats endpoint (#59589)

Przemysław Witek 5 years ago
parent
commit
dfbb47dcaa
16 changed files with 213 additions and 20 deletions
  1. 4 0
      docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc
  2. 4 0
      docs/reference/ml/ml-shared.asciidoc
  3. 21 1
      server/src/main/java/org/elasticsearch/common/Strings.java
  4. 12 0
      server/src/test/java/org/elasticsearch/common/StringsTests.java
  5. 15 4
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDataFrameAnalyticsStatsAction.java
  6. 1 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/classification/ClassificationStats.java
  7. 4 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/classification/ValidationLoss.java
  8. 1 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/regression/RegressionStats.java
  9. 4 1
      x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/regression/ValidationLoss.java
  10. 8 0
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetDataFrameAnalyticsStatsActionRequestTests.java
  11. 38 6
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetDataFrameAnalyticsStatsActionResponseTests.java
  12. 35 1
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/stats/classification/ValidationLossTests.java
  13. 35 1
      x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/stats/regression/ValidationLossTests.java
  14. 10 2
      x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/dataframe/RestGetDataFrameAnalyticsStatsAction.java
  15. 6 0
      x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_data_frame_analytics_stats.json
  16. 15 1
      x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml

+ 4 - 0
docs/reference/ml/df-analytics/apis/get-dfanalytics-stats.asciidoc

@@ -58,6 +58,10 @@ include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=from]
 (Optional, integer) 
 include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=size]
 
+`verbose`::
+(Optional, boolean)
+include::{es-repo-dir}/ml/ml-shared.asciidoc[tag=verbose]
+
 [role="child_attributes"]
 [[ml-get-dfanalytics-stats-response-body]]
 ==== {api-response-body-title}

+ 4 - 0
docs/reference/ml/ml-shared.asciidoc

@@ -1389,3 +1389,7 @@ tag::use-null[]
 Defines whether a new series is used as the null series when there is no value 
 for the by or partition fields. The default value is `false`.
 end::use-null[]
+
+tag::verbose[]
+Defines whether the stats response should be verbose. The default value is `false`.
+end::verbose[]

+ 21 - 1
server/src/main/java/org/elasticsearch/common/Strings.java

@@ -761,6 +761,16 @@ public class Strings {
         return toString(toXContent, false, false);
     }
 
+    /**
+     * Return a {@link String} that is the json representation of the provided {@link ToXContent}.
+     * Wraps the output into an anonymous object if needed.
+     * Allows to configure the params.
+     * The content is not pretty-printed nor human readable.
+     */
+    public static String toString(ToXContent toXContent, ToXContent.Params params) {
+        return toString(toXContent, params, false, false);
+    }
+
     /**
      * Returns a string representation of the builder (only applicable for text based xcontent).
      * @param xContentBuilder builder containing an object to converted to a string
@@ -776,12 +786,22 @@ public class Strings {
      *
      */
     public static String toString(ToXContent toXContent, boolean pretty, boolean human) {
+        return toString(toXContent, ToXContent.EMPTY_PARAMS, pretty, human);
+    }
+
+    /**
+     * Return a {@link String} that is the json representation of the provided {@link ToXContent}.
+     * Wraps the output into an anonymous object if needed.
+     * Allows to configure the params.
+     * Allows to control whether the outputted json needs to be pretty printed and human readable.
+     */
+    private static String toString(ToXContent toXContent, ToXContent.Params params, boolean pretty, boolean human) {
         try {
             XContentBuilder builder = createBuilder(pretty, human);
             if (toXContent.isFragment()) {
                 builder.startObject();
             }
-            toXContent.toXContent(builder, ToXContent.EMPTY_PARAMS);
+            toXContent.toXContent(builder, params);
             if (toXContent.isFragment()) {
                 builder.endObject();
             }

+ 12 - 0
server/src/test/java/org/elasticsearch/common/StringsTests.java

@@ -24,6 +24,8 @@ import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.test.ESTestCase;
 
+import java.util.Collections;
+
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 
@@ -98,6 +100,16 @@ public class StringsTests extends ESTestCase {
         }
     }
 
+    public void testToStringToXContentWithOrWithoutParams() {
+        ToXContent toXContent = (builder, params) -> builder.field("color_from_param", params.param("color", "red"));
+        // Rely on the default value of "color" param when params are not passed
+        assertThat(Strings.toString(toXContent), containsString("\"color_from_param\":\"red\""));
+        // Pass "color" param explicitly
+        assertThat(
+            Strings.toString(toXContent, new ToXContent.MapParams(Collections.singletonMap("color", "blue"))),
+            containsString("\"color_from_param\":\"blue\""));
+    }
+
     public void testSplitStringToSet() {
         assertEquals(Strings.tokenizeByCommaToSet(null), Sets.newHashSet());
         assertEquals(Strings.tokenizeByCommaToSet(""), Sets.newHashSet());

+ 15 - 4
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/GetDataFrameAnalyticsStatsAction.java

@@ -33,6 +33,7 @@ import org.elasticsearch.xpack.core.ml.dataframe.stats.common.MemoryUsage;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.common.DataCounts;
 import org.elasticsearch.xpack.core.ml.utils.ExceptionsHelper;
 import org.elasticsearch.xpack.core.ml.utils.PhaseProgress;
+import org.elasticsearch.xpack.core.ml.utils.ToXContentParams;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -141,7 +142,9 @@ public class GetDataFrameAnalyticsStatsAction extends ActionType<GetDataFrameAna
                 return false;
             }
             Request other = (Request) obj;
-            return Objects.equals(id, other.id) && allowNoMatch == other.allowNoMatch && Objects.equals(pageParams, other.pageParams);
+            return Objects.equals(id, other.id)
+                && allowNoMatch == other.allowNoMatch
+                && Objects.equals(pageParams, other.pageParams);
         }
     }
 
@@ -154,6 +157,9 @@ public class GetDataFrameAnalyticsStatsAction extends ActionType<GetDataFrameAna
 
     public static class Response extends BaseTasksResponse implements ToXContentObject {
 
+        /** Name of the response's REST param which is used to determine whether this response should be verbose. */
+        public static final String VERBOSE = "verbose";
+
         public static class Stats implements ToXContentObject, Writeable {
 
             private final String id;
@@ -295,12 +301,12 @@ public class GetDataFrameAnalyticsStatsAction extends ActionType<GetDataFrameAna
                 // TODO: Have callers wrap the content with an object as they choose rather than forcing it upon them
                 builder.startObject();
                 {
-                    toUnwrappedXContent(builder);
+                    toUnwrappedXContent(builder, params);
                 }
                 return builder.endObject();
             }
 
-            public XContentBuilder toUnwrappedXContent(XContentBuilder builder) throws IOException {
+            private XContentBuilder toUnwrappedXContent(XContentBuilder builder, Params params) throws IOException {
                 builder.field(DataFrameAnalyticsConfig.ID.getPreferredName(), id);
                 builder.field("state", state.toString());
                 if (failureReason != null) {
@@ -313,7 +319,12 @@ public class GetDataFrameAnalyticsStatsAction extends ActionType<GetDataFrameAna
                 builder.field("memory_usage", memoryUsage);
                 if (analysisStats != null) {
                     builder.startObject("analysis_stats");
-                    builder.field(analysisStats.getWriteableName(), analysisStats);
+                    builder.field(
+                        analysisStats.getWriteableName(),
+                        analysisStats,
+                        new MapParams(
+                            Collections.singletonMap(
+                                ToXContentParams.FOR_INTERNAL_STORAGE, Boolean.toString(params.paramAsBoolean(VERBOSE, false)))));
                     builder.endObject();
                 }
                 if (node != null) {

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/classification/ClassificationStats.java

@@ -115,7 +115,7 @@ public class ClassificationStats implements AnalysisStats {
         builder.field(ITERATION.getPreferredName(), iteration);
         builder.field(HYPERPARAMETERS.getPreferredName(), hyperparameters);
         builder.field(TIMING_STATS.getPreferredName(), timingStats);
-        builder.field(VALIDATION_LOSS.getPreferredName(), validationLoss);
+        builder.field(VALIDATION_LOSS.getPreferredName(), validationLoss, params);
         builder.endObject();
         return builder;
     }

+ 4 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/classification/ValidationLoss.java

@@ -14,6 +14,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.common.FoldValues;
+import org.elasticsearch.xpack.core.ml.utils.ToXContentParams;
 
 import java.io.IOException;
 import java.util.List;
@@ -62,7 +63,9 @@ public class ValidationLoss implements ToXContentObject, Writeable {
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
         builder.field(LOSS_TYPE.getPreferredName(), lossType);
-        builder.field(FOLD_VALUES.getPreferredName(), foldValues);
+        if (params.paramAsBoolean(ToXContentParams.FOR_INTERNAL_STORAGE, false)) {
+            builder.field(FOLD_VALUES.getPreferredName(), foldValues);
+        }
         builder.endObject();
         return builder;
     }

+ 1 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/regression/RegressionStats.java

@@ -115,7 +115,7 @@ public class RegressionStats implements AnalysisStats {
         builder.field(ITERATION.getPreferredName(), iteration);
         builder.field(HYPERPARAMETERS.getPreferredName(), hyperparameters);
         builder.field(TIMING_STATS.getPreferredName(), timingStats);
-        builder.field(VALIDATION_LOSS.getPreferredName(), validationLoss);
+        builder.field(VALIDATION_LOSS.getPreferredName(), validationLoss, params);
         builder.endObject();
         return builder;
     }

+ 4 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/dataframe/stats/regression/ValidationLoss.java

@@ -14,6 +14,7 @@ import org.elasticsearch.common.xcontent.ToXContentObject;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.common.FoldValues;
+import org.elasticsearch.xpack.core.ml.utils.ToXContentParams;
 
 import java.io.IOException;
 import java.util.List;
@@ -62,7 +63,9 @@ public class ValidationLoss implements ToXContentObject, Writeable {
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
         builder.field(LOSS_TYPE.getPreferredName(), lossType);
-        builder.field(FOLD_VALUES.getPreferredName(), foldValues);
+        if (params.paramAsBoolean(ToXContentParams.FOR_INTERNAL_STORAGE, false)) {
+            builder.field(FOLD_VALUES.getPreferredName(), foldValues);
+        }
         builder.endObject();
         return builder;
     }

+ 8 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetDataFrameAnalyticsStatsActionRequestTests.java

@@ -9,6 +9,7 @@ package org.elasticsearch.xpack.core.ml.action;
 import org.elasticsearch.test.ESTestCase;
 
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 
 public class GetDataFrameAnalyticsStatsActionRequestTests extends ESTestCase {
@@ -31,4 +32,11 @@ public class GetDataFrameAnalyticsStatsActionRequestTests extends ESTestCase {
 
         assertThat(request.getId(), equalTo("foo"));
     }
+
+    public void testSetAllowNoMatch() {
+        GetDataFrameAnalyticsStatsAction.Request request = new GetDataFrameAnalyticsStatsAction.Request();
+        assertThat(request.isAllowNoMatch(), is(true));
+        request.setAllowNoMatch(false);
+        assertThat(request.isAllowNoMatch(), is(false));
+    }
 }

+ 38 - 6
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/GetDataFrameAnalyticsStatsActionResponseTests.java

@@ -5,8 +5,10 @@
  */
 package org.elasticsearch.xpack.core.ml.action;
 
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
 import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.test.AbstractWireSerializingTestCase;
 import org.elasticsearch.xpack.core.action.util.QueryPage;
 import org.elasticsearch.xpack.core.ml.action.GetDataFrameAnalyticsStatsAction.Response;
@@ -14,6 +16,7 @@ import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfigTests;
 import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsState;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.AnalysisStats;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.AnalysisStatsNamedWriteablesProvider;
+import org.elasticsearch.xpack.core.ml.dataframe.stats.classification.ValidationLoss;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.common.MemoryUsage;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.common.MemoryUsageTests;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.classification.ClassificationStatsTests;
@@ -26,9 +29,12 @@ import org.elasticsearch.xpack.core.ml.utils.PhaseProgress;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Supplier;
 import java.util.stream.IntStream;
 
+import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
 
 public class GetDataFrameAnalyticsStatsActionResponseTests extends AbstractWireSerializingTestCase<Response> {
 
@@ -40,6 +46,17 @@ public class GetDataFrameAnalyticsStatsActionResponseTests extends AbstractWireS
     }
 
     public static Response randomResponse(int listSize) {
+        return randomResponse(
+            listSize,
+            () -> randomBoolean()
+                ? null
+                : randomFrom(
+                    ClassificationStatsTests.createRandom(),
+                    OutlierDetectionStatsTests.createRandom(),
+                    RegressionStatsTests.createRandom()));
+    }
+
+    private static Response randomResponse(int listSize, Supplier<AnalysisStats> analysisStatsSupplier) {
         List<Response.Stats> analytics = new ArrayList<>(listSize);
         for (int j = 0; j < listSize; j++) {
             String failureReason = randomBoolean() ? null : randomAlphaOfLength(10);
@@ -49,12 +66,7 @@ public class GetDataFrameAnalyticsStatsActionResponseTests extends AbstractWireS
                 new PhaseProgress(randomAlphaOfLength(10), randomIntBetween(0, 100))));
             DataCounts dataCounts = randomBoolean() ? null : DataCountsTests.createRandom();
             MemoryUsage memoryUsage = randomBoolean() ? null : MemoryUsageTests.createRandom();
-            AnalysisStats analysisStats = randomBoolean() ? null :
-                randomFrom(
-                    ClassificationStatsTests.createRandom(),
-                    OutlierDetectionStatsTests.createRandom(),
-                    RegressionStatsTests.createRandom()
-                );
+            AnalysisStats analysisStats = analysisStatsSupplier.get();
             Response.Stats stats = new Response.Stats(DataFrameAnalyticsConfigTests.randomValidId(),
                 randomFrom(DataFrameAnalyticsState.values()), failureReason, progress, dataCounts, memoryUsage, analysisStats, null,
                 randomAlphaOfLength(20));
@@ -88,4 +100,24 @@ public class GetDataFrameAnalyticsStatsActionResponseTests extends AbstractWireS
         assertThat(stats.getDataCounts(), equalTo(new DataCounts(stats.getId())));
         assertThat(stats.getMemoryUsage(), equalTo(new MemoryUsage(stats.getId())));
     }
+
+    public void testVerbose() {
+        String foldValuesFieldName = ValidationLoss.FOLD_VALUES.getPreferredName();
+        // Create response for supervised analysis that is certain to contain fold_values field
+        Response response =
+            randomResponse(1, () -> randomFrom(ClassificationStatsTests.createRandom(), RegressionStatsTests.createRandom()));
+
+        // VERBOSE param defaults to "false", fold values *not* outputted
+        assertThat(Strings.toString(response), not(containsString(foldValuesFieldName)));
+
+        // VERBOSE param explicitly set to "false", fold values *not* outputted
+        assertThat(
+            Strings.toString(response, new ToXContent.MapParams(Collections.singletonMap(Response.VERBOSE, "false"))),
+            not(containsString(foldValuesFieldName)));
+
+        // VERBOSE param explicitly set to "true", fold values outputted
+        assertThat(
+            Strings.toString(response, new ToXContent.MapParams(Collections.singletonMap(Response.VERBOSE, "true"))),
+            containsString(foldValuesFieldName));
+    }
 }

+ 35 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/stats/classification/ValidationLossTests.java

@@ -6,13 +6,20 @@
 package org.elasticsearch.xpack.core.ml.dataframe.stats.classification;
 
 import org.elasticsearch.Version;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.xpack.core.ml.AbstractBWCSerializationTestCase;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.common.FoldValuesTests;
+import org.elasticsearch.xpack.core.ml.utils.ToXContentParams;
 import org.junit.Before;
 
 import java.io.IOException;
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
 
 public class ValidationLossTests extends AbstractBWCSerializationTestCase<ValidationLoss> {
 
@@ -28,6 +35,11 @@ public class ValidationLossTests extends AbstractBWCSerializationTestCase<Valida
         return ValidationLoss.fromXContent(parser, lenient);
     }
 
+    @Override
+    protected ToXContent.Params getToXContentParams() {
+        return new ToXContent.MapParams(Collections.singletonMap(ToXContentParams.FOR_INTERNAL_STORAGE, "true"));
+    }
+
     @Override
     protected Writeable.Reader<ValidationLoss> instanceReader() {
         return ValidationLoss::new;
@@ -41,7 +53,7 @@ public class ValidationLossTests extends AbstractBWCSerializationTestCase<Valida
     public static ValidationLoss createRandom() {
         return new ValidationLoss(
             randomAlphaOfLength(10),
-            randomList(5, () -> FoldValuesTests.createRandom())
+            randomList(5, FoldValuesTests::createRandom)
         );
     }
 
@@ -49,4 +61,26 @@ public class ValidationLossTests extends AbstractBWCSerializationTestCase<Valida
     protected ValidationLoss mutateInstanceForVersion(ValidationLoss instance, Version version) {
         return instance;
     }
+
+    public void testValidationLossForStats() {
+        String foldValuesFieldName = ValidationLoss.FOLD_VALUES.getPreferredName();
+        ValidationLoss validationLoss = createTestInstance();
+
+        // FOR_INTERNAL_STORAGE param defaults to "false", fold values *not* outputted
+        assertThat(Strings.toString(validationLoss), not(containsString(foldValuesFieldName)));
+
+        // FOR_INTERNAL_STORAGE param explicitly set to "false", fold values *not* outputted
+        assertThat(
+            Strings.toString(
+                validationLoss,
+                new ToXContent.MapParams(Collections.singletonMap(ToXContentParams.FOR_INTERNAL_STORAGE, "false"))),
+            not(containsString(foldValuesFieldName)));
+
+        // FOR_INTERNAL_STORAGE param explicitly set to "true", fold values are outputted
+        assertThat(
+            Strings.toString(
+                validationLoss,
+                new ToXContent.MapParams(Collections.singletonMap(ToXContentParams.FOR_INTERNAL_STORAGE, "true"))),
+            containsString(foldValuesFieldName));
+    }
 }

+ 35 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/dataframe/stats/regression/ValidationLossTests.java

@@ -6,13 +6,20 @@
 package org.elasticsearch.xpack.core.ml.dataframe.stats.regression;
 
 import org.elasticsearch.Version;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ToXContent;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.xpack.core.ml.AbstractBWCSerializationTestCase;
 import org.elasticsearch.xpack.core.ml.dataframe.stats.common.FoldValuesTests;
+import org.elasticsearch.xpack.core.ml.utils.ToXContentParams;
 import org.junit.Before;
 
 import java.io.IOException;
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.not;
 
 public class ValidationLossTests extends AbstractBWCSerializationTestCase<ValidationLoss> {
 
@@ -28,6 +35,11 @@ public class ValidationLossTests extends AbstractBWCSerializationTestCase<Valida
         return ValidationLoss.fromXContent(parser, lenient);
     }
 
+    @Override
+    protected ToXContent.Params getToXContentParams() {
+        return new ToXContent.MapParams(Collections.singletonMap(ToXContentParams.FOR_INTERNAL_STORAGE, "true"));
+    }
+
     @Override
     protected Writeable.Reader<ValidationLoss> instanceReader() {
         return ValidationLoss::new;
@@ -41,7 +53,7 @@ public class ValidationLossTests extends AbstractBWCSerializationTestCase<Valida
     public static ValidationLoss createRandom() {
         return new ValidationLoss(
             randomAlphaOfLength(10),
-            randomList(5, () -> FoldValuesTests.createRandom())
+            randomList(5, FoldValuesTests::createRandom)
         );
     }
 
@@ -49,4 +61,26 @@ public class ValidationLossTests extends AbstractBWCSerializationTestCase<Valida
     protected ValidationLoss mutateInstanceForVersion(ValidationLoss instance, Version version) {
         return instance;
     }
+
+    public void testValidationLossForStats() {
+        String foldValuesFieldName = ValidationLoss.FOLD_VALUES.getPreferredName();
+        ValidationLoss validationLoss = createTestInstance();
+
+        // FOR_INTERNAL_STORAGE param defaults to "false", fold values *not* outputted
+        assertThat(Strings.toString(validationLoss), not(containsString(foldValuesFieldName)));
+
+        // FOR_INTERNAL_STORAGE param explicitly set to "false", fold values *not* outputted
+        assertThat(
+            Strings.toString(
+                validationLoss,
+                new ToXContent.MapParams(Collections.singletonMap(ToXContentParams.FOR_INTERNAL_STORAGE, "false"))),
+            not(containsString(foldValuesFieldName)));
+
+        // FOR_INTERNAL_STORAGE param explicitly set to "true", fold values are outputted
+        assertThat(
+            Strings.toString(
+                validationLoss,
+                new ToXContent.MapParams(Collections.singletonMap(ToXContentParams.FOR_INTERNAL_STORAGE, "true"))),
+            containsString(foldValuesFieldName));
+    }
 }

+ 10 - 2
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/dataframe/RestGetDataFrameAnalyticsStatsAction.java

@@ -16,7 +16,9 @@ import org.elasticsearch.xpack.core.ml.dataframe.DataFrameAnalyticsConfig;
 import org.elasticsearch.xpack.ml.MachineLearning;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 import static org.elasticsearch.rest.RestRequest.Method.GET;
 
@@ -46,9 +48,15 @@ public class RestGetDataFrameAnalyticsStatsAction extends BaseRestHandler {
             request.setPageParams(new PageParams(restRequest.paramAsInt(PageParams.FROM.getPreferredName(), PageParams.DEFAULT_FROM),
                     restRequest.paramAsInt(PageParams.SIZE.getPreferredName(), PageParams.DEFAULT_SIZE)));
         }
-        request.setAllowNoMatch(restRequest.paramAsBoolean(GetDataFrameAnalyticsStatsAction.Request.ALLOW_NO_MATCH.getPreferredName(),
-            request.isAllowNoMatch()));
+        request.setAllowNoMatch(
+            restRequest.paramAsBoolean(
+                GetDataFrameAnalyticsStatsAction.Request.ALLOW_NO_MATCH.getPreferredName(), request.isAllowNoMatch()));
 
         return channel -> client.execute(GetDataFrameAnalyticsStatsAction.INSTANCE, request, new RestToXContentListener<>(channel));
     }
+
+    @Override
+    protected Set<String> responseParams() {
+        return Collections.singleton(GetDataFrameAnalyticsStatsAction.Response.VERBOSE);
+    }
 }

+ 6 - 0
x-pack/plugin/src/test/resources/rest-api-spec/api/ml.get_data_frame_analytics_stats.json

@@ -43,6 +43,12 @@
         "type":"int",
         "description":"specifies a max number of analytics to get",
         "default":100
+      },
+      "verbose":{
+        "type":"boolean",
+        "required":false,
+        "description":"whether the stats response should be verbose",
+        "default":false
       }
     }
   }

+ 15 - 1
x-pack/plugin/src/test/resources/rest-api-spec/test/ml/data_frame_analytics_crud.yml

@@ -903,7 +903,7 @@ setup:
   - match: { data_frame_analytics.0.state: "stopped" }
 
 ---
-"Test get stats on newly created congig":
+"Test get stats on newly created config":
 
   - do:
       ml.put_data_frame_analytics:
@@ -933,6 +933,20 @@ setup:
   - match: { data_frame_analytics.0.memory_usage.peak_usage_bytes: 0 }
   - match: { data_frame_analytics.0.memory_usage.status: "ok" }
 
+  - do:
+      ml.get_data_frame_analytics_stats:
+        id: "foo-1"
+        verbose: true
+  - match: { count: 1 }
+  - length: { data_frame_analytics: 1 }
+  - match: { data_frame_analytics.0.id: "foo-1" }
+  - match: { data_frame_analytics.0.state: "stopped" }
+  - match: { data_frame_analytics.0.data_counts.training_docs_count: 0 }
+  - match: { data_frame_analytics.0.data_counts.test_docs_count: 0 }
+  - match: { data_frame_analytics.0.data_counts.skipped_docs_count: 0 }
+  - match: { data_frame_analytics.0.memory_usage.peak_usage_bytes: 0 }
+  - match: { data_frame_analytics.0.memory_usage.status: "ok" }
+
 ---
 "Test delete given stopped config":