[ML] Add created_by info to usage stats (#40518)

This change adds information about which UI path
(if any) created ML anomaly detector jobs to the
stats returned by the _xpack/usage endpoint.

Counts for the following possibilities are expected:

* ml_module_apache_access
* ml_module_apm_transaction
* ml_module_auditbeat_process_docker
* ml_module_auditbeat_process_hosts
* ml_module_nginx_access
* ml_module_sample
* multi_metric_wizard
* population_wizard
* single_metric_wizard
* unknown

The "unknown" count is for jobs that do not have a
created_by setting in their custom_settings.

Closes #38403
This commit is contained in:
David Roberts 2019-04-04 10:43:30 +01:00
parent c8047c0644
commit d16f86f7ab
3 changed files with 38 additions and 6 deletions

View File

@ -25,6 +25,7 @@ public class MachineLearningFeatureSetUsage extends XPackFeatureSet.Usage {
public static final String DETECTORS = "detectors";
public static final String FORECASTS = "forecasts";
public static final String MODEL_SIZE = "model_size";
public static final String CREATED_BY = "created_by";
public static final String NODE_COUNT = "node_count";
private final Map<String, Object> jobsUsage;

View File

@ -216,13 +216,16 @@ public class MachineLearningFeatureSet implements XPackFeatureSet {
Map<JobState, StatsAccumulator> detectorStatsByState = new HashMap<>();
Map<JobState, StatsAccumulator> modelSizeStatsByState = new HashMap<>();
Map<JobState, ForecastStats> forecastStatsByState = new HashMap<>();
Map<JobState, Map<String, Long>> createdByByState = new HashMap<>();
List<GetJobsStatsAction.Response.JobStats> jobsStats = response.getResponse().results();
Map<String, Job> jobMap = jobs.stream().collect(Collectors.toMap(Job::getId, item -> item));
Map<String, Long> allJobsCreatedBy = jobs.stream().map(this::jobCreatedBy)
.collect(Collectors.groupingBy(item -> item, Collectors.counting()));;
for (GetJobsStatsAction.Response.JobStats jobStats : jobsStats) {
ModelSizeStats modelSizeStats = jobStats.getModelSizeStats();
int detectorsCount = jobMap.get(jobStats.getJobId()).getAnalysisConfig()
.getDetectors().size();
Job job = jobMap.get(jobStats.getJobId());
int detectorsCount = job.getAnalysisConfig().getDetectors().size();
double modelSize = modelSizeStats == null ? 0.0
: jobStats.getModelSizeStats().getModelBytes();
@ -237,27 +240,41 @@ public class MachineLearningFeatureSet implements XPackFeatureSet {
modelSizeStatsByState.computeIfAbsent(jobState,
js -> new StatsAccumulator()).add(modelSize);
forecastStatsByState.merge(jobState, jobStats.getForecastStats(), (f1, f2) -> f1.merge(f2));
createdByByState.computeIfAbsent(jobState, js -> new HashMap<>())
.compute(jobCreatedBy(job), (k, v) -> (v == null) ? 1L : (v + 1));
}
jobsUsage.put(MachineLearningFeatureSetUsage.ALL, createJobUsageEntry(jobs.size(), allJobsDetectorsStats,
allJobsModelSizeStats, allJobsForecastStats));
allJobsModelSizeStats, allJobsForecastStats, allJobsCreatedBy));
for (JobState jobState : jobCountByState.keySet()) {
jobsUsage.put(jobState.name().toLowerCase(Locale.ROOT), createJobUsageEntry(
jobCountByState.get(jobState).get(),
detectorStatsByState.get(jobState),
modelSizeStatsByState.get(jobState),
forecastStatsByState.get(jobState)));
forecastStatsByState.get(jobState),
createdByByState.get(jobState)));
}
}
private String jobCreatedBy(Job job) {
Map<String, Object> customSettings = job.getCustomSettings();
if (customSettings == null || customSettings.containsKey(MachineLearningFeatureSetUsage.CREATED_BY) == false) {
return "unknown";
}
// Replace non-alpha-numeric characters with underscores because
// the values from custom settings become keys in the usage data
return customSettings.get(MachineLearningFeatureSetUsage.CREATED_BY).toString().replaceAll("\\W", "_");
}
private Map<String, Object> createJobUsageEntry(long count, StatsAccumulator detectorStats,
StatsAccumulator modelSizeStats,
ForecastStats forecastStats) {
ForecastStats forecastStats, Map<String, Long> createdBy) {
Map<String, Object> usage = new HashMap<>();
usage.put(MachineLearningFeatureSetUsage.COUNT, count);
usage.put(MachineLearningFeatureSetUsage.DETECTORS, detectorStats.asMap());
usage.put(MachineLearningFeatureSetUsage.MODEL_SIZE, modelSizeStats.asMap());
usage.put(MachineLearningFeatureSetUsage.FORECASTS, forecastStats.asMap());
usage.put(MachineLearningFeatureSetUsage.CREATED_BY, createdBy);
return usage;
}

View File

@ -155,7 +155,8 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
Settings.Builder settings = Settings.builder().put(commonSettings);
settings.put("xpack.ml.enabled", true);
Job opened1 = buildJob("opened1", Arrays.asList(buildMinDetector("foo")));
Job opened1 = buildJob("opened1", Collections.singletonList(buildMinDetector("foo")),
Collections.singletonMap("created_by", randomFrom("a-cool-module", "a_cool_module", "a cool module")));
GetJobsStatsAction.Response.JobStats opened1JobStats = buildJobStats("opened1", JobState.OPENED, 100L, 3L);
Job opened2 = buildJob("opened2", Arrays.asList(buildMinDetector("foo"), buildMinDetector("bar")));
GetJobsStatsAction.Response.JobStats opened2JobStats = buildJobStats("opened2", JobState.OPENED, 200L, 8L);
@ -200,6 +201,8 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
assertThat(source.getValue("jobs._all.model_size.max"), equalTo(300.0));
assertThat(source.getValue("jobs._all.model_size.total"), equalTo(600.0));
assertThat(source.getValue("jobs._all.model_size.avg"), equalTo(200.0));
assertThat(source.getValue("jobs._all.created_by.a_cool_module"), equalTo(1));
assertThat(source.getValue("jobs._all.created_by.unknown"), equalTo(2));
assertThat(source.getValue("jobs.opened.count"), equalTo(2));
assertThat(source.getValue("jobs.opened.detectors.min"), equalTo(1.0));
@ -210,6 +213,8 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
assertThat(source.getValue("jobs.opened.model_size.max"), equalTo(200.0));
assertThat(source.getValue("jobs.opened.model_size.total"), equalTo(300.0));
assertThat(source.getValue("jobs.opened.model_size.avg"), equalTo(150.0));
assertThat(source.getValue("jobs.opened.created_by.a_cool_module"), equalTo(1));
assertThat(source.getValue("jobs.opened.created_by.unknown"), equalTo(1));
assertThat(source.getValue("jobs.closed.count"), equalTo(1));
assertThat(source.getValue("jobs.closed.detectors.min"), equalTo(3.0));
@ -220,6 +225,8 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
assertThat(source.getValue("jobs.closed.model_size.max"), equalTo(300.0));
assertThat(source.getValue("jobs.closed.model_size.total"), equalTo(300.0));
assertThat(source.getValue("jobs.closed.model_size.avg"), equalTo(300.0));
assertThat(source.getValue("jobs.closed.created_by.a_cool_module"), is(nullValue()));
assertThat(source.getValue("jobs.closed.created_by.unknown"), equalTo(1));
assertThat(source.getValue("jobs.opening"), is(nullValue()));
assertThat(source.getValue("jobs.closing"), is(nullValue()));
@ -359,6 +366,7 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
}).when(jobManager).expandJobs(eq(MetaData.ALL), eq(true), any(ActionListener.class));
doAnswer(invocationOnMock -> {
@SuppressWarnings("unchecked")
ActionListener<GetJobsStatsAction.Response> listener =
(ActionListener<GetJobsStatsAction.Response>) invocationOnMock.getArguments()[2];
listener.onResponse(new GetJobsStatsAction.Response(
@ -400,6 +408,7 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
private void givenDatafeeds(List<GetDatafeedsStatsAction.Response.DatafeedStats> datafeedStats) {
doAnswer(invocationOnMock -> {
@SuppressWarnings("unchecked")
ActionListener<GetDatafeedsStatsAction.Response> listener =
(ActionListener<GetDatafeedsStatsAction.Response>) invocationOnMock.getArguments()[2];
listener.onResponse(new GetDatafeedsStatsAction.Response(
@ -416,10 +425,15 @@ public class MachineLearningFeatureSetTests extends ESTestCase {
}
private static Job buildJob(String jobId, List<Detector> detectors) {
return buildJob(jobId, detectors, null);
}
private static Job buildJob(String jobId, List<Detector> detectors, Map<String, Object> customSettings) {
AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(detectors);
return new Job.Builder(jobId)
.setAnalysisConfig(analysisConfig)
.setDataDescription(new DataDescription.Builder())
.setCustomSettings(customSettings)
.build(new Date(randomNonNegativeLong()));
}