Skip to content

Commit 999f934

Browse files
authored
Formalize usage stats for analytics (#52966)
This moves the usage statistics gathering from the `AnalyticsPlugin` into an `AnalyicsUsage`, removing the static state. It also checks the license level when parsing all analytics aggregations. This is how we were checking them before but we did it in an easy to forget way. This way is slightly simpler, I think.
1 parent 4b33908 commit 999f934

File tree

7 files changed

+188
-72
lines changed

7 files changed

+188
-72
lines changed

x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/AnalyticsPlugin.java

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,25 @@
77

88
import org.elasticsearch.action.ActionRequest;
99
import org.elasticsearch.action.ActionResponse;
10+
import org.elasticsearch.client.Client;
11+
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
12+
import org.elasticsearch.cluster.service.ClusterService;
13+
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1014
import org.elasticsearch.common.settings.Setting;
1115
import org.elasticsearch.common.xcontent.ContextParser;
16+
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
17+
import org.elasticsearch.env.Environment;
18+
import org.elasticsearch.env.NodeEnvironment;
1219
import org.elasticsearch.index.mapper.Mapper;
20+
import org.elasticsearch.license.LicenseUtils;
1321
import org.elasticsearch.license.XPackLicenseState;
1422
import org.elasticsearch.plugins.ActionPlugin;
1523
import org.elasticsearch.plugins.MapperPlugin;
1624
import org.elasticsearch.plugins.Plugin;
1725
import org.elasticsearch.plugins.SearchPlugin;
26+
import org.elasticsearch.script.ScriptService;
27+
import org.elasticsearch.threadpool.ThreadPool;
28+
import org.elasticsearch.watcher.ResourceWatcherService;
1829
import org.elasticsearch.xpack.analytics.action.AnalyticsInfoTransportAction;
1930
import org.elasticsearch.xpack.analytics.action.AnalyticsUsageTransportAction;
2031
import org.elasticsearch.xpack.analytics.action.TransportAnalyticsStatsAction;
@@ -28,24 +39,22 @@
2839
import org.elasticsearch.xpack.analytics.topmetrics.InternalTopMetrics;
2940
import org.elasticsearch.xpack.analytics.topmetrics.TopMetricsAggregationBuilder;
3041
import org.elasticsearch.xpack.analytics.topmetrics.TopMetricsAggregatorFactory;
42+
import org.elasticsearch.xpack.core.XPackField;
3143
import org.elasticsearch.xpack.core.XPackPlugin;
3244
import org.elasticsearch.xpack.core.action.XPackInfoFeatureAction;
3345
import org.elasticsearch.xpack.core.action.XPackUsageFeatureAction;
3446
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
3547

3648
import java.util.Arrays;
49+
import java.util.Collection;
3750
import java.util.Collections;
3851
import java.util.List;
3952
import java.util.Map;
40-
import java.util.concurrent.atomic.AtomicLong;
4153

4254
import static java.util.Collections.singletonList;
4355

4456
public class AnalyticsPlugin extends Plugin implements SearchPlugin, ActionPlugin, MapperPlugin {
45-
46-
// TODO this should probably become more structured
47-
public static AtomicLong cumulativeCardUsage = new AtomicLong(0);
48-
public static AtomicLong topMetricsUsage = new AtomicLong(0);
57+
private final AnalyticsUsage usage = new AnalyticsUsage();
4958

5059
public AnalyticsPlugin() { }
5160

@@ -58,7 +67,8 @@ public List<PipelineAggregationSpec> getPipelineAggregations() {
5867
CumulativeCardinalityPipelineAggregationBuilder.NAME,
5968
CumulativeCardinalityPipelineAggregationBuilder::new,
6069
CumulativeCardinalityPipelineAggregator::new,
61-
CumulativeCardinalityPipelineAggregationBuilder.PARSER)
70+
usage.track(AnalyticsUsage.Item.CUMULATIVE_CARDINALITY,
71+
checkLicense(CumulativeCardinalityPipelineAggregationBuilder.PARSER)))
6272
);
6373
}
6474

@@ -68,16 +78,17 @@ public List<AggregationSpec> getAggregations() {
6878
new AggregationSpec(
6979
StringStatsAggregationBuilder.NAME,
7080
StringStatsAggregationBuilder::new,
71-
StringStatsAggregationBuilder.PARSER).addResultReader(InternalStringStats::new),
81+
usage.track(AnalyticsUsage.Item.STRING_STATS, checkLicense(StringStatsAggregationBuilder.PARSER)))
82+
.addResultReader(InternalStringStats::new),
7283
new AggregationSpec(
7384
BoxplotAggregationBuilder.NAME,
7485
BoxplotAggregationBuilder::new,
75-
BoxplotAggregationBuilder.PARSER)
86+
usage.track(AnalyticsUsage.Item.BOXPLOT, checkLicense(BoxplotAggregationBuilder.PARSER)))
7687
.addResultReader(InternalBoxplot::new),
7788
new AggregationSpec(
7889
TopMetricsAggregationBuilder.NAME,
7990
TopMetricsAggregationBuilder::new,
80-
track(TopMetricsAggregationBuilder.PARSER, topMetricsUsage))
91+
usage.track(AnalyticsUsage.Item.TOP_METRICS, checkLicense(TopMetricsAggregationBuilder.PARSER)))
8192
.addResultReader(InternalTopMetrics::new)
8293
);
8394
}
@@ -100,15 +111,20 @@ public Map<String, Mapper.TypeParser> getMappers() {
100111
return Collections.singletonMap(HistogramFieldMapper.CONTENT_TYPE, new HistogramFieldMapper.TypeParser());
101112
}
102113

103-
/**
104-
* Track successful parsing.
105-
*/
106-
private static <T> ContextParser<String, T> track(ContextParser<String, T> realParser, AtomicLong usage) {
114+
@Override
115+
public Collection<Object> createComponents(Client client, ClusterService clusterService, ThreadPool threadPool,
116+
ResourceWatcherService resourceWatcherService, ScriptService scriptService, NamedXContentRegistry xContentRegistry,
117+
Environment environment, NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry,
118+
IndexNameExpressionResolver indexNameExpressionResolver) {
119+
return singletonList(new AnalyticsUsage());
120+
}
121+
122+
private static <T> ContextParser<String, T> checkLicense(ContextParser<String, T> realParser) {
107123
return (parser, name) -> {
108-
T value = realParser.parse(parser, name);
109-
// Intentionally doesn't count unless the parser returns cleanly.
110-
usage.addAndGet(1);
111-
return value;
124+
if (getLicenseState().isDataScienceAllowed() == false) {
125+
throw LicenseUtils.newComplianceException(XPackField.ANALYTICS);
126+
}
127+
return realParser.parse(parser, name);
112128
};
113129
}
114130
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.analytics;
8+
9+
import org.elasticsearch.cluster.node.DiscoveryNode;
10+
import org.elasticsearch.common.xcontent.ContextParser;
11+
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
12+
13+
import java.util.EnumMap;
14+
import java.util.Map;
15+
import java.util.concurrent.atomic.AtomicLong;
16+
17+
/**
18+
* Tracks usage of the Analytics aggregations.
19+
*/
20+
public class AnalyticsUsage {
21+
/**
22+
* Items to track.
23+
*/
24+
public enum Item {
25+
BOXPLOT,
26+
CUMULATIVE_CARDINALITY,
27+
STRING_STATS,
28+
TOP_METRICS;
29+
}
30+
31+
private final Map<Item, AtomicLong> trackers = new EnumMap<>(Item.class);
32+
33+
public AnalyticsUsage() {
34+
for (Item item: Item.values()) {
35+
trackers.put(item, new AtomicLong(0));
36+
}
37+
}
38+
39+
/**
40+
* Track successful parsing.
41+
*/
42+
public <C, T> ContextParser<C, T> track(Item item, ContextParser<C, T> realParser) {
43+
AtomicLong usage = trackers.get(item);
44+
return (parser, context) -> {
45+
T value = realParser.parse(parser, context);
46+
// Intentionally doesn't count unless the parser returns cleanly.
47+
usage.incrementAndGet();
48+
return value;
49+
};
50+
}
51+
52+
public AnalyticsStatsAction.NodeResponse stats(DiscoveryNode node) {
53+
return new AnalyticsStatsAction.NodeResponse(node,
54+
trackers.get(Item.BOXPLOT).get(),
55+
trackers.get(Item.CUMULATIVE_CARDINALITY).get(),
56+
trackers.get(Item.STRING_STATS).get(),
57+
trackers.get(Item.TOP_METRICS).get());
58+
}
59+
}

x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/action/TransportAnalyticsStatsAction.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,23 @@
1414
import org.elasticsearch.tasks.Task;
1515
import org.elasticsearch.threadpool.ThreadPool;
1616
import org.elasticsearch.transport.TransportService;
17+
import org.elasticsearch.xpack.analytics.AnalyticsUsage;
1718
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
18-
import org.elasticsearch.xpack.analytics.AnalyticsPlugin;
1919

2020
import java.io.IOException;
2121
import java.util.List;
2222

2323
public class TransportAnalyticsStatsAction extends TransportNodesAction<AnalyticsStatsAction.Request, AnalyticsStatsAction.Response,
24-
AnalyticsStatsAction.NodeRequest, AnalyticsStatsAction.NodeResponse> {
25-
24+
AnalyticsStatsAction.NodeRequest, AnalyticsStatsAction.NodeResponse> {
25+
private final AnalyticsUsage usage;
2626

2727
@Inject
2828
public TransportAnalyticsStatsAction(TransportService transportService, ClusterService clusterService,
29-
ThreadPool threadPool, ActionFilters actionFilters) {
29+
ThreadPool threadPool, ActionFilters actionFilters, AnalyticsUsage usage) {
3030
super(AnalyticsStatsAction.NAME, threadPool, clusterService, transportService, actionFilters,
3131
AnalyticsStatsAction.Request::new, AnalyticsStatsAction.NodeRequest::new, ThreadPool.Names.MANAGEMENT,
3232
AnalyticsStatsAction.NodeResponse.class);
33+
this.usage = usage;
3334
}
3435

3536
@Override
@@ -51,10 +52,7 @@ protected AnalyticsStatsAction.NodeResponse newNodeResponse(StreamInput in) thro
5152

5253
@Override
5354
protected AnalyticsStatsAction.NodeResponse nodeOperation(AnalyticsStatsAction.NodeRequest request, Task task) {
54-
AnalyticsStatsAction.NodeResponse statsResponse = new AnalyticsStatsAction.NodeResponse(clusterService.localNode());
55-
statsResponse.setCumulativeCardinalityUsage(AnalyticsPlugin.cumulativeCardUsage.get());
56-
statsResponse.setTopMetricsUsage(AnalyticsPlugin.topMetricsUsage.get());
57-
return statsResponse;
55+
return usage.stats(clusterService.localNode());
5856
}
5957

6058
}

x-pack/plugin/analytics/src/main/java/org/elasticsearch/xpack/analytics/cumulativecardinality/CumulativeCardinalityPipelineAggregationBuilder.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,13 @@
99
import org.elasticsearch.common.io.stream.StreamOutput;
1010
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
1111
import org.elasticsearch.common.xcontent.XContentBuilder;
12-
import org.elasticsearch.license.LicenseUtils;
1312
import org.elasticsearch.search.DocValueFormat;
1413
import org.elasticsearch.search.aggregations.AggregationBuilder;
1514
import org.elasticsearch.search.aggregations.AggregatorFactory;
1615
import org.elasticsearch.search.aggregations.PipelineAggregationBuilder;
1716
import org.elasticsearch.search.aggregations.pipeline.AbstractPipelineAggregationBuilder;
1817
import org.elasticsearch.search.aggregations.pipeline.BucketMetricsParser;
1918
import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator;
20-
import org.elasticsearch.xpack.analytics.AnalyticsPlugin;
21-
import org.elasticsearch.xpack.core.XPackField;
2219

2320
import java.io.IOException;
2421
import java.util.Collection;
@@ -35,14 +32,6 @@ public class CumulativeCardinalityPipelineAggregationBuilder
3532

3633
public static final ConstructingObjectParser<CumulativeCardinalityPipelineAggregationBuilder, String> PARSER =
3734
new ConstructingObjectParser<>(NAME, false, (args, name) -> {
38-
if (AnalyticsPlugin.getLicenseState().isDataScienceAllowed() == false) {
39-
throw LicenseUtils.newComplianceException(XPackField.ANALYTICS);
40-
}
41-
42-
// Increment usage here since it is a good boundary between internal and external, and should correlate 1:1 with
43-
// usage and not internal instantiations
44-
AnalyticsPlugin.cumulativeCardUsage.incrementAndGet();
45-
4635
return new CumulativeCardinalityPipelineAggregationBuilder(name, (String) args[0]);
4736
});
4837
static {

x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/action/TransportAnalyticsStatsActionTests.java

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,81 @@
1313
import org.elasticsearch.cluster.node.DiscoveryNode;
1414
import org.elasticsearch.cluster.service.ClusterService;
1515
import org.elasticsearch.common.bytes.BytesReference;
16+
import org.elasticsearch.common.xcontent.ContextParser;
1617
import org.elasticsearch.common.xcontent.ToXContent;
1718
import org.elasticsearch.common.xcontent.XContentBuilder;
1819
import org.elasticsearch.common.xcontent.json.JsonXContent;
1920
import org.elasticsearch.test.ESTestCase;
2021
import org.elasticsearch.test.rest.yaml.ObjectPath;
2122
import org.elasticsearch.threadpool.ThreadPool;
2223
import org.elasticsearch.transport.TransportService;
24+
import org.elasticsearch.xpack.analytics.AnalyticsUsage;
2325
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
24-
import org.junit.Before;
2526

27+
import java.io.IOException;
2628
import java.util.Arrays;
2729
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Locale;
2832

33+
import static java.util.Collections.emptyList;
34+
import static java.util.stream.Collectors.toList;
2935
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
3036
import static org.hamcrest.Matchers.equalTo;
3137
import static org.mockito.Mockito.mock;
3238
import static org.mockito.Mockito.when;
3339

3440
public class TransportAnalyticsStatsActionTests extends ESTestCase {
35-
36-
private TransportAnalyticsStatsAction action;
37-
38-
@Before
39-
public void setupTransportAction() {
41+
public TransportAnalyticsStatsAction action(AnalyticsUsage usage) {
4042
TransportService transportService = mock(TransportService.class);
4143
ThreadPool threadPool = mock(ThreadPool.class);
4244

4345
ClusterService clusterService = mock(ClusterService.class);
4446
DiscoveryNode discoveryNode = new DiscoveryNode("nodeId", buildNewFakeTransportAddress(), Version.CURRENT);
4547
when(clusterService.localNode()).thenReturn(discoveryNode);
46-
4748
ClusterName clusterName = new ClusterName("cluster_name");
4849
when(clusterService.getClusterName()).thenReturn(clusterName);
4950

51+
5052
ClusterState clusterState = mock(ClusterState.class);
5153
when(clusterState.getMetaData()).thenReturn(MetaData.EMPTY_META_DATA);
5254
when(clusterService.state()).thenReturn(clusterState);
5355

56+
return new TransportAnalyticsStatsAction(transportService, clusterService, threadPool,
57+
new ActionFilters(Collections.emptySet()), usage);
58+
}
5459

55-
action = new TransportAnalyticsStatsAction(transportService, clusterService, threadPool, new
56-
ActionFilters(Collections.emptySet()));
60+
public void test() throws IOException {
61+
AnalyticsUsage.Item item = randomFrom(AnalyticsUsage.Item.values());
62+
AnalyticsUsage realUsage = new AnalyticsUsage();
63+
AnalyticsUsage emptyUsage = new AnalyticsUsage();
64+
ContextParser<Void, Void> parser = realUsage.track(item, (p, c) -> c);
65+
ObjectPath unused = run(realUsage, emptyUsage);
66+
assertThat(unused.evaluate("stats.0." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(0));
67+
assertThat(unused.evaluate("stats.1." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(0));
68+
int count = between(1, 10000);
69+
for (int i = 0; i < count; i++) {
70+
assertNull(parser.parse(null, null));
71+
}
72+
ObjectPath used = run(realUsage, emptyUsage);
73+
assertThat(used.evaluate("stats.0." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(count));
74+
assertThat(used.evaluate("stats.1." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(0));
5775
}
5876

59-
public void testCumulativeCardStats() throws Exception {
77+
private ObjectPath run(AnalyticsUsage... nodeUsages) throws IOException {
6078
AnalyticsStatsAction.Request request = new AnalyticsStatsAction.Request();
61-
AnalyticsStatsAction.NodeResponse nodeResponse1 = action.nodeOperation(new AnalyticsStatsAction.NodeRequest(request), null);
62-
AnalyticsStatsAction.NodeResponse nodeResponse2 = action.nodeOperation(new AnalyticsStatsAction.NodeRequest(request), null);
63-
64-
AnalyticsStatsAction.Response response = action.newResponse(request,
65-
Arrays.asList(nodeResponse1, nodeResponse2), Collections.emptyList());
79+
List<AnalyticsStatsAction.NodeResponse> nodeResponses = Arrays.stream(nodeUsages)
80+
.map(usage -> action(usage).nodeOperation(new AnalyticsStatsAction.NodeRequest(request), null))
81+
.collect(toList());
82+
AnalyticsStatsAction.Response response = new AnalyticsStatsAction.Response(
83+
new ClusterName("cluster_name"), nodeResponses, emptyList());
6684

6785
try (XContentBuilder builder = jsonBuilder()) {
6886
builder.startObject();
6987
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
7088
builder.endObject();
7189

72-
ObjectPath objectPath = ObjectPath.createFromXContent(JsonXContent.jsonXContent, BytesReference.bytes(builder));
73-
assertThat(objectPath.evaluate("stats.0.cumulative_cardinality_usage"), equalTo(0));
74-
assertThat(objectPath.evaluate("stats.1.cumulative_cardinality_usage"), equalTo(0));
90+
return ObjectPath.createFromXContent(JsonXContent.jsonXContent, BytesReference.bytes(builder));
7591
}
7692
}
7793
}

x-pack/plugin/analytics/src/test/java/org/elasticsearch/xpack/analytics/boxplot/BoxplotAggregationBuilderTests.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,18 @@
66

77
package org.elasticsearch.xpack.analytics.boxplot;
88

9+
import org.elasticsearch.common.ParseField;
910
import org.elasticsearch.common.io.stream.Writeable;
10-
import org.elasticsearch.common.settings.Settings;
1111
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
1212
import org.elasticsearch.common.xcontent.XContentParser;
13-
import org.elasticsearch.search.SearchModule;
1413
import org.elasticsearch.search.aggregations.AggregatorFactories;
14+
import org.elasticsearch.search.aggregations.BaseAggregationBuilder;
1515
import org.elasticsearch.test.AbstractSerializingTestCase;
16-
import org.elasticsearch.xpack.analytics.AnalyticsPlugin;
1716
import org.junit.Before;
1817

1918
import java.io.IOException;
20-
import java.util.Collections;
2119

20+
import static java.util.Collections.singletonList;
2221
import static org.hamcrest.Matchers.hasSize;
2322

2423
public class BoxplotAggregationBuilderTests extends AbstractSerializingTestCase<BoxplotAggregationBuilder> {
@@ -31,8 +30,10 @@ public void setupName() {
3130

3231
@Override
3332
protected NamedXContentRegistry xContentRegistry() {
34-
SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.singletonList(new AnalyticsPlugin()));
35-
return new NamedXContentRegistry(searchModule.getNamedXContents());
33+
return new NamedXContentRegistry(singletonList(new NamedXContentRegistry.Entry(
34+
BaseAggregationBuilder.class,
35+
new ParseField(BoxplotAggregationBuilder.NAME),
36+
(p, n) -> BoxplotAggregationBuilder.PARSER.apply(p, (String) n))));
3637
}
3738

3839
@Override

0 commit comments

Comments
 (0)