Adjust validation endpoints (elastic/elasticsearch#812)

Changes are:

1. The detector validation endpoint is changed from /_xpack/ml/_validate/detector
   to /_xpack/ml/anomaly_detectors/_validate/detector
2. A new endpoint is added for validating an entire job config:
   /_xpack/ml/anomaly_detectors/_validate

Relates elastic/elasticsearch#630

Original commit: elastic/x-pack-elasticsearch@7b2031e746
This commit is contained in:
David Roberts 2017-01-30 17:10:22 +00:00 committed by GitHub
parent 4eab74ce29
commit ab957b6d91
11 changed files with 426 additions and 41 deletions

View File

@ -25,6 +25,8 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsFilter;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.ml.action.ValidateJobConfigAction;
import org.elasticsearch.xpack.ml.rest.validate.RestValidateJobConfigAction;
import org.elasticsearch.xpack.persistent.RemovePersistentTaskAction;
import org.elasticsearch.xpack.persistent.PersistentActionCoordinator;
import org.elasticsearch.xpack.persistent.PersistentActionRegistry;
@ -271,6 +273,7 @@ public class MlPlugin extends Plugin implements ActionPlugin {
new RestCloseJobAction(settings, restController),
new RestFlushJobAction(settings, restController),
new RestValidateDetectorAction(settings, restController),
new RestValidateJobConfigAction(settings, restController),
new RestGetCategoriesAction(settings, restController),
new RestGetModelSnapshotsAction(settings, restController),
new RestRevertModelSnapshotAction(settings, restController),
@ -309,6 +312,7 @@ public class MlPlugin extends Plugin implements ActionPlugin {
new ActionHandler<>(CloseJobAction.INSTANCE, CloseJobAction.TransportAction.class),
new ActionHandler<>(FlushJobAction.INSTANCE, FlushJobAction.TransportAction.class),
new ActionHandler<>(ValidateDetectorAction.INSTANCE, ValidateDetectorAction.TransportAction.class),
new ActionHandler<>(ValidateJobConfigAction.INSTANCE, ValidateJobConfigAction.TransportAction.class),
new ActionHandler<>(GetCategoriesAction.INSTANCE, GetCategoriesAction.TransportAction.class),
new ActionHandler<>(GetModelSnapshotsAction.INSTANCE, GetModelSnapshotsAction.TransportAction.class),
new ActionHandler<>(RevertModelSnapshotAction.INSTANCE, RevertModelSnapshotAction.TransportAction.class),

View File

@ -34,7 +34,7 @@ public class ValidateDetectorAction
extends Action<ValidateDetectorAction.Request, ValidateDetectorAction.Response, ValidateDetectorAction.RequestBuilder> {
public static final ValidateDetectorAction INSTANCE = new ValidateDetectorAction();
public static final String NAME = "cluster:admin/ml/validate/detector";
public static final String NAME = "cluster:admin/ml/anomaly_detectors/validate/detector";
protected ValidateDetectorAction() {
super(NAME);

View File

@ -0,0 +1,164 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ml.action;
import org.elasticsearch.action.Action;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.ml.job.config.Job;
import java.io.IOException;
import java.util.Objects;
public class ValidateJobConfigAction
extends Action<ValidateJobConfigAction.Request, ValidateJobConfigAction.Response, ValidateJobConfigAction.RequestBuilder> {
public static final ValidateJobConfigAction INSTANCE = new ValidateJobConfigAction();
public static final String NAME = "cluster:admin/ml/anomaly_detectors/validate";
protected ValidateJobConfigAction() {
super(NAME);
}
@Override
public RequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new RequestBuilder(client, INSTANCE);
}
@Override
public Response newResponse() {
return new Response();
}
public static class RequestBuilder extends ActionRequestBuilder<Request, Response, RequestBuilder> {
protected RequestBuilder(ElasticsearchClient client, ValidateJobConfigAction action) {
super(client, action, new Request());
}
}
public static class Request extends ActionRequest implements ToXContent {
private Job job;
public static Request parseRequest(XContentParser parser) {
Job.Builder job = Job.PARSER.apply(parser, null);
// When jobs are PUT their ID must be supplied in the URL - assume this will
// be valid unless an invalid job ID is specified in the JSON to be validated
return new Request(job.build(true, (job.getId() != null) ? job.getId() : "ok"));
}
Request() {
this.job = null;
}
public Request(Job job) {
this.job = job;
}
public Job getJob() {
return job;
}
@Override
public ActionRequestValidationException validate() {
return null;
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
job.writeTo(out);
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
job = new Job(in);
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
job.toXContent(builder, params);
return builder;
}
@Override
public int hashCode() {
return Objects.hash(job);
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Request other = (Request) obj;
return Objects.equals(job, other.job);
}
}
public static class Response extends AcknowledgedResponse {
public Response() {
super();
}
public Response(boolean acknowledged) {
super(acknowledged);
}
@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
readAcknowledged(in);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
writeAcknowledged(out);
}
}
public static class TransportAction extends HandledTransportAction<Request, Response> {
@Inject
public TransportAction(Settings settings, TransportService transportService, ClusterService clusterService, ThreadPool threadPool,
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver) {
super(settings, ValidateJobConfigAction.NAME, threadPool, transportService, actionFilters, indexNameExpressionResolver,
Request::new);
}
@Override
protected void doExecute(Request request, ActionListener<Response> listener) {
listener.onResponse(new Response(true));
}
}
}

View File

@ -71,6 +71,9 @@ public class Job extends AbstractDiffable<Job> implements Writeable, ToXContent
public static final ObjectParser<Builder, Void> PARSER = new ObjectParser<>("job_details", Builder::new);
public static final int MAX_JOB_ID_LENGTH = 64;
public static final long MIN_BACKGROUND_PERSIST_INTERVAL = 3600;
static {
PARSER.declareString(Builder::setId, ID);
PARSER.declareStringOrNull(Builder::setDescription, DESCRIPTION);
@ -140,6 +143,30 @@ public class Job extends AbstractDiffable<Job> implements Writeable, ToXContent
ModelDebugConfig modelDebugConfig, IgnoreDowntime ignoreDowntime,
Long renormalizationWindowDays, Long backgroundPersistInterval, Long modelSnapshotRetentionDays, Long resultsRetentionDays,
Map<String, Object> customSettings, String modelSnapshotId, String indexName) {
if (analysisConfig == null) {
throw new IllegalArgumentException(Messages.getMessage(Messages.JOB_CONFIG_MISSING_ANALYSISCONFIG));
}
checkValueNotLessThan(0, "timeout", timeout);
checkValueNotLessThan(0, "renormalizationWindowDays", renormalizationWindowDays);
checkValueNotLessThan(MIN_BACKGROUND_PERSIST_INTERVAL, "backgroundPersistInterval", backgroundPersistInterval);
checkValueNotLessThan(0, "modelSnapshotRetentionDays", modelSnapshotRetentionDays);
checkValueNotLessThan(0, "resultsRetentionDays", resultsRetentionDays);
if (!MlStrings.isValidId(jobId)) {
throw new IllegalArgumentException(Messages.getMessage(Messages.INVALID_ID, ID.getPreferredName(), jobId));
}
if (jobId.length() > MAX_JOB_ID_LENGTH) {
throw new IllegalArgumentException(Messages.getMessage(Messages.JOB_CONFIG_ID_TOO_LONG, MAX_JOB_ID_LENGTH));
}
if (Strings.isNullOrEmpty(indexName)) {
indexName = jobId;
} else if (!MlStrings.isValidId(indexName)) {
throw new IllegalArgumentException(Messages.getMessage(Messages.INVALID_ID, INDEX_NAME.getPreferredName()));
}
this.jobId = jobId;
this.description = description;
this.createTime = createTime;
@ -496,10 +523,14 @@ public class Job extends AbstractDiffable<Job> implements Writeable, ToXContent
}
}
private static void checkValueNotLessThan(long minVal, String name, Long value) {
if (value != null && value < minVal) {
throw new IllegalArgumentException(Messages.getMessage(Messages.JOB_CONFIG_FIELD_VALUE_TOO_LOW, name, minVal, value));
}
}
public static class Builder {
public static final int MAX_JOB_ID_LENGTH = 64;
public static final long MIN_BACKGROUND_PERSIST_INTERVAL = 3600;
public static final long DEFAULT_TIMEOUT = 600;
private String id;
@ -641,15 +672,6 @@ public class Job extends AbstractDiffable<Job> implements Writeable, ToXContent
}
public Job build(boolean fromApi, String urlJobId) {
if (analysisConfig == null) {
throw new IllegalArgumentException(Messages.getMessage(Messages.JOB_CONFIG_MISSING_ANALYSISCONFIG));
}
checkValueNotLessThan(0, "timeout", timeout);
checkValueNotLessThan(0, "renormalizationWindowDays", renormalizationWindowDays);
checkValueNotLessThan(MIN_BACKGROUND_PERSIST_INTERVAL, "backgroundPersistInterval", backgroundPersistInterval);
checkValueNotLessThan(0, "modelSnapshotRetentionDays", modelSnapshotRetentionDays);
checkValueNotLessThan(0, "resultsRetentionDays", resultsRetentionDays);
Date createTime;
Date finishedTime;
@ -672,19 +694,6 @@ public class Job extends AbstractDiffable<Job> implements Writeable, ToXContent
modelSnapshotId = this.modelSnapshotId;
}
if (!MlStrings.isValidId(id)) {
throw new IllegalArgumentException(Messages.getMessage(Messages.INVALID_ID, ID.getPreferredName(), id));
}
if (id.length() > MAX_JOB_ID_LENGTH) {
throw new IllegalArgumentException(Messages.getMessage(Messages.JOB_CONFIG_ID_TOO_LONG, MAX_JOB_ID_LENGTH));
}
if (Strings.isNullOrEmpty(indexName)) {
indexName = id;
} else if (!MlStrings.isValidId(indexName)) {
throw new IllegalArgumentException(Messages.getMessage(Messages.INVALID_ID, INDEX_NAME.getPreferredName()));
}
return new Job(
id, description, createTime, finishedTime, lastDataTime, timeout, analysisConfig, analysisLimits,
dataDescription, modelDebugConfig, ignoreDowntime, renormalizationWindowDays,
@ -692,11 +701,5 @@ public class Job extends AbstractDiffable<Job> implements Writeable, ToXContent
indexName
);
}
private static void checkValueNotLessThan(long minVal, String name, Long value) {
if (value != null && value < minVal) {
throw new IllegalArgumentException(Messages.getMessage(Messages.JOB_CONFIG_FIELD_VALUE_TOO_LOW, name, minVal, value));
}
}
}
}

View File

@ -21,7 +21,7 @@ public class RestValidateDetectorAction extends BaseRestHandler {
public RestValidateDetectorAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(RestRequest.Method.POST, MlPlugin.BASE_PATH + "_validate/detector", this);
controller.registerHandler(RestRequest.Method.POST, MlPlugin.BASE_PATH + "anomaly_detectors/_validate/detector", this);
}
@Override

View File

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ml.rest.validate;
import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.action.AcknowledgedRestListener;
import org.elasticsearch.xpack.ml.MlPlugin;
import org.elasticsearch.xpack.ml.action.ValidateJobConfigAction;
import java.io.IOException;
public class RestValidateJobConfigAction extends BaseRestHandler {
public RestValidateJobConfigAction(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(RestRequest.Method.POST, MlPlugin.BASE_PATH + "anomaly_detectors/_validate", this);
}
@Override
protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) throws IOException {
XContentParser parser = restRequest.contentOrSourceParamParser();
ValidateJobConfigAction.Request validateConfigRequest = ValidateJobConfigAction.Request.parseRequest(parser);
return channel ->
client.execute(ValidateJobConfigAction.INSTANCE, validateConfigRequest, new AcknowledgedRestListener<>(channel));
}
}

View File

@ -31,8 +31,8 @@ public class GetJobsActionResponseTests extends AbstractStreamableTestCase<GetJo
int listSize = randomInt(10);
List<Job> jobList = new ArrayList<>(listSize);
for (int j = 0; j < listSize; j++) {
String jobId = randomAsciiOfLength(10);
String description = randomBoolean() ? randomAsciiOfLength(10) : null;
String jobId = "job" + j;
String description = randomBoolean() ? randomAsciiOfLength(100) : null;
Date createTime = new Date(randomNonNegativeLong());
Date finishedTime = randomBoolean() ? new Date(randomNonNegativeLong()) : null;
Date lastDataTime = randomBoolean() ? new Date(randomNonNegativeLong()) : null;
@ -43,14 +43,14 @@ public class GetJobsActionResponseTests extends AbstractStreamableTestCase<GetJo
DataDescription dataDescription = randomBoolean() ? new DataDescription.Builder().build() : null;
ModelDebugConfig modelDebugConfig = randomBoolean() ? new ModelDebugConfig(randomDouble(), randomAsciiOfLength(10)) : null;
IgnoreDowntime ignoreDowntime = randomFrom(IgnoreDowntime.values());
Long normalizationWindowDays = randomBoolean() ? randomLong() : null;
Long backgroundPersistInterval = randomBoolean() ? randomLong() : null;
Long modelSnapshotRetentionDays = randomBoolean() ? randomLong() : null;
Long resultsRetentionDays = randomBoolean() ? randomLong() : null;
Long normalizationWindowDays = randomBoolean() ? Long.valueOf(randomIntBetween(0, 365)) : null;
Long backgroundPersistInterval = randomBoolean() ? Long.valueOf(randomIntBetween(3600, 86400)) : null;
Long modelSnapshotRetentionDays = randomBoolean() ? Long.valueOf(randomIntBetween(0, 365)) : null;
Long resultsRetentionDays = randomBoolean() ? Long.valueOf(randomIntBetween(0, 365)) : null;
Map<String, Object> customConfig = randomBoolean() ? Collections.singletonMap(randomAsciiOfLength(10), randomAsciiOfLength(10))
: null;
String modelSnapshotId = randomBoolean() ? randomAsciiOfLength(10) : null;
String indexName = randomAsciiOfLength(10);
String indexName = randomBoolean() ? "index" + j : null;
Job job = new Job(jobId, description, createTime, finishedTime, lastDataTime,
timeout, analysisConfig, analysisLimits, dataDescription,
modelDebugConfig, ignoreDowntime, normalizationWindowDays, backgroundPersistInterval,

View File

@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.ml.action;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.xpack.ml.action.ValidateJobConfigAction.Request;
import org.elasticsearch.xpack.ml.job.config.AnalysisConfig;
import org.elasticsearch.xpack.ml.job.config.AnalysisLimits;
import org.elasticsearch.xpack.ml.job.config.DataDescription;
import org.elasticsearch.xpack.ml.job.config.Detector;
import org.elasticsearch.xpack.ml.job.config.Job;
import org.elasticsearch.xpack.ml.support.AbstractStreamableXContentTestCase;
import java.util.ArrayList;
import java.util.List;
public class ValidateJobConfigActionRequestTests extends AbstractStreamableXContentTestCase<ValidateJobConfigAction.Request> {
@Override
protected Request createTestInstance() {
List<Detector> detectors = new ArrayList<>();
detectors.add(new Detector.Builder(randomFrom(Detector.FIELD_NAME_FUNCTIONS), randomAsciiOfLengthBetween(1, 20)).build());
detectors.add(new Detector.Builder(randomFrom(Detector.COUNT_WITHOUT_FIELD_FUNCTIONS), null).build());
AnalysisConfig.Builder analysisConfigBuilder = new AnalysisConfig.Builder(detectors);
analysisConfigBuilder.setBucketSpan(randomIntBetween(60, 86400));
if (randomBoolean()) {
analysisConfigBuilder.setLatency(randomIntBetween(0, 12));
}
if (randomBoolean()) {
analysisConfigBuilder.setCategorizationFieldName(randomAsciiOfLengthBetween(1, 20));
}
if (randomBoolean()) {
analysisConfigBuilder.setSummaryCountFieldName(randomAsciiOfLengthBetween(1, 20));
}
if (randomBoolean()) {
List<String> influencers = new ArrayList<>();
for (int i = randomIntBetween(1, 5); i > 0; --i) {
influencers.add(randomAsciiOfLengthBetween(1, 20));
}
analysisConfigBuilder.setInfluencers(influencers);
}
if (randomBoolean()) {
analysisConfigBuilder.setOverlappingBuckets(randomBoolean());
}
if (randomBoolean()) {
analysisConfigBuilder.setMultivariateByFields(randomBoolean());
}
Job.Builder job = new Job.Builder("ok");
job.setAnalysisConfig(analysisConfigBuilder);
if (randomBoolean()) {
DataDescription.Builder dataDescription = new DataDescription.Builder();
if (randomBoolean()) {
dataDescription.setFormat(DataDescription.DataFormat.DELIMITED);
if (randomBoolean()) {
dataDescription.setFieldDelimiter(';');
}
if (randomBoolean()) {
dataDescription.setQuoteCharacter('\'');
}
} else {
dataDescription.setFormat(DataDescription.DataFormat.JSON);
}
dataDescription.setTimeField(randomAsciiOfLengthBetween(1, 20));
if (randomBoolean()) {
dataDescription.setTimeFormat("yyyy-MM-dd HH:mm:ssX");
}
job.setDataDescription(dataDescription);
}
if (randomBoolean()) {
job.setAnalysisLimits(new AnalysisLimits(randomNonNegativeLong(), randomNonNegativeLong()));
}
return new Request(job.build(true, "ok"));
}
@Override
protected Request createBlankInstance() {
return new Request();
}
@Override
protected Request parseInstance(XContentParser parser) {
return Request.parseRequest(parser);
}
}

View File

@ -0,0 +1,14 @@
{
"xpack.ml.validate": {
"methods": [ "POST" ],
"url": {
"path": "/_xpack/ml/anomaly_detectors/_validate",
"paths": [ "/_xpack/ml/anomaly_detectors/_validate" ],
"params": {}
},
"body": {
"description" : "The job config",
"required" : true
}
}
}

View File

@ -2,8 +2,8 @@
"xpack.ml.validate_detector": {
"methods": [ "POST" ],
"url": {
"path": "/_xpack/ml/_validate/detector",
"paths": [ "/_xpack/ml/_validate/detector" ],
"path": "/_xpack/ml/anomaly_detectors/_validate/detector",
"paths": [ "/_xpack/ml/anomaly_detectors/_validate/detector" ],
"params": {}
},
"body": {

View File

@ -0,0 +1,77 @@
---
"Test valid job config":
- do:
xpack.ml.validate:
body: >
{
"analysis_config": {
"bucket_span": 3600,
"detectors": [{"function": "metric", "field_name": "responsetime", "by_field_name": "airline"}]
},
"data_description": {
"format": "delimited",
"field_delimiter": ",",
"time_field": "time",
"time_format": "yyyy-MM-dd HH:mm:ssX"
}
}
- match: { acknowledged: true }
---
"Test invalid job config":
- do:
catch: /.data_description. failed to parse field .format./
xpack.ml.validate:
body: >
{
"analysis_config": {
"bucket_span": 3600,
"detectors": [{"function": "metric", "field_name": "responsetime", "by_field_name": "airline"}]
},
"data_description": {
"format": "wrong",
"field_delimiter": ",",
"time_field": "time",
"time_format": "yyyy-MM-dd HH:mm:ssX"
}
}
---
"Test valid job config with job ID":
- do:
xpack.ml.validate:
body: >
{
"job_id": "farequote",
"analysis_config": {
"bucket_span": 3600,
"detectors": [{"function": "metric", "field_name": "responsetime", "by_field_name": "airline"}]
},
"data_description": {
"format": "delimited",
"field_delimiter": ",",
"time_field": "time",
"time_format": "yyyy-MM-dd HH:mm:ssX"
}
}
- match: { acknowledged: true }
---
"Test job config that's invalid only because of the job ID":
- do:
catch: /Invalid job_id; '_' must be lowercase alphanumeric, may contain hyphens or underscores, may not start with underscore/
xpack.ml.validate:
body: >
{
"job_id": "_",
"analysis_config": {
"bucket_span": 3600,
"detectors": [{"function": "metric", "field_name": "responsetime", "by_field_name": "airline"}]
},
"data_description": {
"format": "delimited",
"field_delimiter": ",",
"time_field": "time",
"time_format": "yyyy-MM-dd HH:mm:ssX"
}
}