Skip to content

Commit d16f86f

Browse files
author
David Roberts
committed
[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
1 parent c8047c0 commit d16f86f

File tree

3 files changed

+38
-6
lines changed

3 files changed

+38
-6
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/MachineLearningFeatureSetUsage.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class MachineLearningFeatureSetUsage extends XPackFeatureSet.Usage {
2525
public static final String DETECTORS = "detectors";
2626
public static final String FORECASTS = "forecasts";
2727
public static final String MODEL_SIZE = "model_size";
28+
public static final String CREATED_BY = "created_by";
2829
public static final String NODE_COUNT = "node_count";
2930

3031
private final Map<String, Object> jobsUsage;

x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearningFeatureSet.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,16 @@ private void addJobsUsage(GetJobsStatsAction.Response response, List<Job> jobs)
216216
Map<JobState, StatsAccumulator> detectorStatsByState = new HashMap<>();
217217
Map<JobState, StatsAccumulator> modelSizeStatsByState = new HashMap<>();
218218
Map<JobState, ForecastStats> forecastStatsByState = new HashMap<>();
219+
Map<JobState, Map<String, Long>> createdByByState = new HashMap<>();
219220

220221
List<GetJobsStatsAction.Response.JobStats> jobsStats = response.getResponse().results();
221222
Map<String, Job> jobMap = jobs.stream().collect(Collectors.toMap(Job::getId, item -> item));
223+
Map<String, Long> allJobsCreatedBy = jobs.stream().map(this::jobCreatedBy)
224+
.collect(Collectors.groupingBy(item -> item, Collectors.counting()));;
222225
for (GetJobsStatsAction.Response.JobStats jobStats : jobsStats) {
223226
ModelSizeStats modelSizeStats = jobStats.getModelSizeStats();
224-
int detectorsCount = jobMap.get(jobStats.getJobId()).getAnalysisConfig()
225-
.getDetectors().size();
227+
Job job = jobMap.get(jobStats.getJobId());
228+
int detectorsCount = job.getAnalysisConfig().getDetectors().size();
226229
double modelSize = modelSizeStats == null ? 0.0
227230
: jobStats.getModelSizeStats().getModelBytes();
228231

@@ -237,27 +240,41 @@ private void addJobsUsage(GetJobsStatsAction.Response response, List<Job> jobs)
237240
modelSizeStatsByState.computeIfAbsent(jobState,
238241
js -> new StatsAccumulator()).add(modelSize);
239242
forecastStatsByState.merge(jobState, jobStats.getForecastStats(), (f1, f2) -> f1.merge(f2));
243+
createdByByState.computeIfAbsent(jobState, js -> new HashMap<>())
244+
.compute(jobCreatedBy(job), (k, v) -> (v == null) ? 1L : (v + 1));
240245
}
241246

242247
jobsUsage.put(MachineLearningFeatureSetUsage.ALL, createJobUsageEntry(jobs.size(), allJobsDetectorsStats,
243-
allJobsModelSizeStats, allJobsForecastStats));
248+
allJobsModelSizeStats, allJobsForecastStats, allJobsCreatedBy));
244249
for (JobState jobState : jobCountByState.keySet()) {
245250
jobsUsage.put(jobState.name().toLowerCase(Locale.ROOT), createJobUsageEntry(
246251
jobCountByState.get(jobState).get(),
247252
detectorStatsByState.get(jobState),
248253
modelSizeStatsByState.get(jobState),
249-
forecastStatsByState.get(jobState)));
254+
forecastStatsByState.get(jobState),
255+
createdByByState.get(jobState)));
250256
}
251257
}
252258

259+
private String jobCreatedBy(Job job) {
260+
Map<String, Object> customSettings = job.getCustomSettings();
261+
if (customSettings == null || customSettings.containsKey(MachineLearningFeatureSetUsage.CREATED_BY) == false) {
262+
return "unknown";
263+
}
264+
// Replace non-alpha-numeric characters with underscores because
265+
// the values from custom settings become keys in the usage data
266+
return customSettings.get(MachineLearningFeatureSetUsage.CREATED_BY).toString().replaceAll("\\W", "_");
267+
}
268+
253269
private Map<String, Object> createJobUsageEntry(long count, StatsAccumulator detectorStats,
254270
StatsAccumulator modelSizeStats,
255-
ForecastStats forecastStats) {
271+
ForecastStats forecastStats, Map<String, Long> createdBy) {
256272
Map<String, Object> usage = new HashMap<>();
257273
usage.put(MachineLearningFeatureSetUsage.COUNT, count);
258274
usage.put(MachineLearningFeatureSetUsage.DETECTORS, detectorStats.asMap());
259275
usage.put(MachineLearningFeatureSetUsage.MODEL_SIZE, modelSizeStats.asMap());
260276
usage.put(MachineLearningFeatureSetUsage.FORECASTS, forecastStats.asMap());
277+
usage.put(MachineLearningFeatureSetUsage.CREATED_BY, createdBy);
261278
return usage;
262279
}
263280

x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningFeatureSetTests.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ public void testUsage() throws Exception {
155155
Settings.Builder settings = Settings.builder().put(commonSettings);
156156
settings.put("xpack.ml.enabled", true);
157157

158-
Job opened1 = buildJob("opened1", Arrays.asList(buildMinDetector("foo")));
158+
Job opened1 = buildJob("opened1", Collections.singletonList(buildMinDetector("foo")),
159+
Collections.singletonMap("created_by", randomFrom("a-cool-module", "a_cool_module", "a cool module")));
159160
GetJobsStatsAction.Response.JobStats opened1JobStats = buildJobStats("opened1", JobState.OPENED, 100L, 3L);
160161
Job opened2 = buildJob("opened2", Arrays.asList(buildMinDetector("foo"), buildMinDetector("bar")));
161162
GetJobsStatsAction.Response.JobStats opened2JobStats = buildJobStats("opened2", JobState.OPENED, 200L, 8L);
@@ -200,6 +201,8 @@ public void testUsage() throws Exception {
200201
assertThat(source.getValue("jobs._all.model_size.max"), equalTo(300.0));
201202
assertThat(source.getValue("jobs._all.model_size.total"), equalTo(600.0));
202203
assertThat(source.getValue("jobs._all.model_size.avg"), equalTo(200.0));
204+
assertThat(source.getValue("jobs._all.created_by.a_cool_module"), equalTo(1));
205+
assertThat(source.getValue("jobs._all.created_by.unknown"), equalTo(2));
203206

204207
assertThat(source.getValue("jobs.opened.count"), equalTo(2));
205208
assertThat(source.getValue("jobs.opened.detectors.min"), equalTo(1.0));
@@ -210,6 +213,8 @@ public void testUsage() throws Exception {
210213
assertThat(source.getValue("jobs.opened.model_size.max"), equalTo(200.0));
211214
assertThat(source.getValue("jobs.opened.model_size.total"), equalTo(300.0));
212215
assertThat(source.getValue("jobs.opened.model_size.avg"), equalTo(150.0));
216+
assertThat(source.getValue("jobs.opened.created_by.a_cool_module"), equalTo(1));
217+
assertThat(source.getValue("jobs.opened.created_by.unknown"), equalTo(1));
213218

214219
assertThat(source.getValue("jobs.closed.count"), equalTo(1));
215220
assertThat(source.getValue("jobs.closed.detectors.min"), equalTo(3.0));
@@ -220,6 +225,8 @@ public void testUsage() throws Exception {
220225
assertThat(source.getValue("jobs.closed.model_size.max"), equalTo(300.0));
221226
assertThat(source.getValue("jobs.closed.model_size.total"), equalTo(300.0));
222227
assertThat(source.getValue("jobs.closed.model_size.avg"), equalTo(300.0));
228+
assertThat(source.getValue("jobs.closed.created_by.a_cool_module"), is(nullValue()));
229+
assertThat(source.getValue("jobs.closed.created_by.unknown"), equalTo(1));
223230

224231
assertThat(source.getValue("jobs.opening"), is(nullValue()));
225232
assertThat(source.getValue("jobs.closing"), is(nullValue()));
@@ -359,6 +366,7 @@ private void givenJobs(List<Job> jobs, List<GetJobsStatsAction.Response.JobStats
359366
}).when(jobManager).expandJobs(eq(MetaData.ALL), eq(true), any(ActionListener.class));
360367

361368
doAnswer(invocationOnMock -> {
369+
@SuppressWarnings("unchecked")
362370
ActionListener<GetJobsStatsAction.Response> listener =
363371
(ActionListener<GetJobsStatsAction.Response>) invocationOnMock.getArguments()[2];
364372
listener.onResponse(new GetJobsStatsAction.Response(
@@ -400,6 +408,7 @@ private void givenNodeCount(int nodeCount) {
400408

401409
private void givenDatafeeds(List<GetDatafeedsStatsAction.Response.DatafeedStats> datafeedStats) {
402410
doAnswer(invocationOnMock -> {
411+
@SuppressWarnings("unchecked")
403412
ActionListener<GetDatafeedsStatsAction.Response> listener =
404413
(ActionListener<GetDatafeedsStatsAction.Response>) invocationOnMock.getArguments()[2];
405414
listener.onResponse(new GetDatafeedsStatsAction.Response(
@@ -416,10 +425,15 @@ private static Detector buildMinDetector(String fieldName) {
416425
}
417426

418427
private static Job buildJob(String jobId, List<Detector> detectors) {
428+
return buildJob(jobId, detectors, null);
429+
}
430+
431+
private static Job buildJob(String jobId, List<Detector> detectors, Map<String, Object> customSettings) {
419432
AnalysisConfig.Builder analysisConfig = new AnalysisConfig.Builder(detectors);
420433
return new Job.Builder(jobId)
421434
.setAnalysisConfig(analysisConfig)
422435
.setDataDescription(new DataDescription.Builder())
436+
.setCustomSettings(customSettings)
423437
.build(new Date(randomNonNegativeLong()));
424438
}
425439

0 commit comments

Comments
 (0)