Skip to content

Commit e593f3e

Browse files
authored
Add analytics plugin usage stats to _xpack/usage (#54911)
Adds analytics plugin usage stats to _xpack/usage. Closes #54847
1 parent 998a085 commit e593f3e

File tree

14 files changed

+656
-143
lines changed

14 files changed

+656
-143
lines changed

docs/reference/rest-api/usage.asciidoc

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Provides usage information about the installed {xpack} features.
1616
=== {api-description-title}
1717

1818
This API provides information about which features are currently enabled and
19-
available under the current license and some usage statistics.
19+
available under the current license and some usage statistics.
2020

2121
[discrete]
2222
[[usage-api-query-parms]]
@@ -263,7 +263,14 @@ GET /_xpack/usage
263263
},
264264
"analytics" : {
265265
"available" : true,
266-
"enabled" : true
266+
"enabled" : true,
267+
"stats": {
268+
"boxplot_usage" : 0,
269+
"top_metrics_usage" : 0,
270+
"cumulative_cardinality_usage" : 0,
271+
"t_test_usage" : 0,
272+
"string_stats_usage" : 0
273+
}
267274
}
268275
}
269276
------------------------------------------------------------

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public List<PipelineAggregationSpec> getPipelineAggregations() {
7575
CumulativeCardinalityPipelineAggregationBuilder.NAME,
7676
CumulativeCardinalityPipelineAggregationBuilder::new,
7777
CumulativeCardinalityPipelineAggregator::new,
78-
usage.track(AnalyticsUsage.Item.CUMULATIVE_CARDINALITY,
78+
usage.track(AnalyticsStatsAction.Item.CUMULATIVE_CARDINALITY,
7979
checkLicense(CumulativeCardinalityPipelineAggregationBuilder.PARSER)))
8080
);
8181
}
@@ -86,24 +86,24 @@ public List<AggregationSpec> getAggregations() {
8686
new AggregationSpec(
8787
StringStatsAggregationBuilder.NAME,
8888
StringStatsAggregationBuilder::new,
89-
usage.track(AnalyticsUsage.Item.STRING_STATS, checkLicense(StringStatsAggregationBuilder.PARSER)))
89+
usage.track(AnalyticsStatsAction.Item.STRING_STATS, checkLicense(StringStatsAggregationBuilder.PARSER)))
9090
.addResultReader(InternalStringStats::new)
9191
.setAggregatorRegistrar(StringStatsAggregationBuilder::registerAggregators),
9292
new AggregationSpec(
9393
BoxplotAggregationBuilder.NAME,
9494
BoxplotAggregationBuilder::new,
95-
usage.track(AnalyticsUsage.Item.BOXPLOT, checkLicense(BoxplotAggregationBuilder.PARSER)))
95+
usage.track(AnalyticsStatsAction.Item.BOXPLOT, checkLicense(BoxplotAggregationBuilder.PARSER)))
9696
.addResultReader(InternalBoxplot::new)
9797
.setAggregatorRegistrar(BoxplotAggregationBuilder::registerAggregators),
9898
new AggregationSpec(
9999
TopMetricsAggregationBuilder.NAME,
100100
TopMetricsAggregationBuilder::new,
101-
usage.track(AnalyticsUsage.Item.TOP_METRICS, checkLicense(TopMetricsAggregationBuilder.PARSER)))
101+
usage.track(AnalyticsStatsAction.Item.TOP_METRICS, checkLicense(TopMetricsAggregationBuilder.PARSER)))
102102
.addResultReader(InternalTopMetrics::new),
103103
new AggregationSpec(
104104
TTestAggregationBuilder.NAME,
105105
TTestAggregationBuilder::new,
106-
usage.track(AnalyticsUsage.Item.T_TEST, checkLicense(TTestAggregationBuilder.PARSER)))
106+
usage.track(AnalyticsStatsAction.Item.T_TEST, checkLicense(TTestAggregationBuilder.PARSER)))
107107
.addResultReader(InternalTTest::new)
108108
);
109109
}
@@ -137,7 +137,7 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
137137
ResourceWatcherService resourceWatcherService, ScriptService scriptService, NamedXContentRegistry xContentRegistry,
138138
Environment environment, NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry,
139139
IndexNameExpressionResolver indexNameExpressionResolver) {
140-
return singletonList(new AnalyticsUsage());
140+
return singletonList(usage);
141141
}
142142

143143
@Override

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

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,54 +8,32 @@
88

99
import org.elasticsearch.cluster.node.DiscoveryNode;
1010
import org.elasticsearch.common.xcontent.ContextParser;
11+
import org.elasticsearch.xpack.core.analytics.EnumCounters;
1112
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
1213

13-
import java.util.EnumMap;
14-
import java.util.Map;
15-
import java.util.concurrent.atomic.AtomicLong;
16-
1714
/**
1815
* Tracks usage of the Analytics aggregations.
1916
*/
2017
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-
T_TEST;
30-
}
3118

32-
private final Map<Item, AtomicLong> trackers = new EnumMap<>(Item.class);
19+
private final EnumCounters<AnalyticsStatsAction.Item> counters = new EnumCounters<>(AnalyticsStatsAction.Item.class);
3320

3421
public AnalyticsUsage() {
35-
for (Item item: Item.values()) {
36-
trackers.put(item, new AtomicLong(0));
37-
}
3822
}
3923

4024
/**
4125
* Track successful parsing.
4226
*/
43-
public <C, T> ContextParser<C, T> track(Item item, ContextParser<C, T> realParser) {
44-
AtomicLong usage = trackers.get(item);
27+
public <C, T> ContextParser<C, T> track(AnalyticsStatsAction.Item item, ContextParser<C, T> realParser) {
4528
return (parser, context) -> {
4629
T value = realParser.parse(parser, context);
4730
// Intentionally doesn't count unless the parser returns cleanly.
48-
usage.incrementAndGet();
31+
counters.inc(item);
4932
return value;
5033
};
5134
}
5235

5336
public AnalyticsStatsAction.NodeResponse stats(DiscoveryNode node) {
54-
return new AnalyticsStatsAction.NodeResponse(node,
55-
trackers.get(Item.BOXPLOT).get(),
56-
trackers.get(Item.CUMULATIVE_CARDINALITY).get(),
57-
trackers.get(Item.STRING_STATS).get(),
58-
trackers.get(Item.TOP_METRICS).get(),
59-
trackers.get(Item.T_TEST).get());
37+
return new AnalyticsStatsAction.NodeResponse(node, counters);
6038
}
6139
}

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import org.elasticsearch.action.ActionListener;
99
import org.elasticsearch.action.support.ActionFilters;
10+
import org.elasticsearch.client.Client;
1011
import org.elasticsearch.cluster.ClusterState;
1112
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
1213
import org.elasticsearch.cluster.service.ClusterService;
@@ -20,26 +21,49 @@
2021
import org.elasticsearch.xpack.core.action.XPackUsageFeatureResponse;
2122
import org.elasticsearch.xpack.core.action.XPackUsageFeatureTransportAction;
2223
import org.elasticsearch.xpack.core.analytics.AnalyticsFeatureSetUsage;
24+
import org.elasticsearch.xpack.core.analytics.EnumCounters;
25+
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
26+
27+
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.stream.Collectors;
2330

2431
public class AnalyticsUsageTransportAction extends XPackUsageFeatureTransportAction {
2532
private final XPackLicenseState licenseState;
33+
private final Client client;
2634

2735
@Inject
2836
public AnalyticsUsageTransportAction(TransportService transportService, ClusterService clusterService, ThreadPool threadPool,
2937
ActionFilters actionFilters, IndexNameExpressionResolver indexNameExpressionResolver,
30-
XPackLicenseState licenseState) {
38+
XPackLicenseState licenseState, Client client) {
3139
super(XPackUsageFeatureAction.ANALYTICS.name(), transportService, clusterService,
3240
threadPool, actionFilters, indexNameExpressionResolver);
3341
this.licenseState = licenseState;
42+
this.client = client;
3443
}
3544

3645
@Override
3746
protected void masterOperation(Task task, XPackUsageRequest request, ClusterState state,
3847
ActionListener<XPackUsageFeatureResponse> listener) {
3948
boolean available = licenseState.isDataScienceAllowed();
49+
if (available) {
50+
AnalyticsStatsAction.Request statsRequest = new AnalyticsStatsAction.Request();
51+
statsRequest.setParentTask(clusterService.localNode().getId(), task.getId());
52+
client.execute(AnalyticsStatsAction.INSTANCE, statsRequest, ActionListener.wrap(r ->
53+
listener.onResponse(new XPackUsageFeatureResponse(usageFeatureResponse(true, true, r))),
54+
listener::onFailure));
55+
} else {
56+
AnalyticsFeatureSetUsage usage = new AnalyticsFeatureSetUsage(false, true, Collections.emptyMap());
57+
listener.onResponse(new XPackUsageFeatureResponse(usage));
58+
}
59+
}
4060

41-
AnalyticsFeatureSetUsage usage =
42-
new AnalyticsFeatureSetUsage(available, true);
43-
listener.onResponse(new XPackUsageFeatureResponse(usage));
61+
static AnalyticsFeatureSetUsage usageFeatureResponse(boolean available, boolean enabled, AnalyticsStatsAction.Response r) {
62+
List<EnumCounters<AnalyticsStatsAction.Item>> countersPerNode = r.getNodes()
63+
.stream()
64+
.map(AnalyticsStatsAction.NodeResponse::getStats)
65+
.collect(Collectors.toList());
66+
EnumCounters<AnalyticsStatsAction.Item> mergedCounters = EnumCounters.merge(AnalyticsStatsAction.Item.class, countersPerNode);
67+
return new AnalyticsFeatureSetUsage(available, enabled, mergedCounters.toMap());
4468
}
4569
}

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

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,54 @@
55
*/
66
package org.elasticsearch.xpack.analytics.action;
77

8+
import org.elasticsearch.action.ActionListener;
89
import org.elasticsearch.action.support.ActionFilters;
910
import org.elasticsearch.action.support.PlainActionFuture;
11+
import org.elasticsearch.client.Client;
12+
import org.elasticsearch.cluster.ClusterName;
13+
import org.elasticsearch.cluster.node.DiscoveryNode;
14+
import org.elasticsearch.cluster.service.ClusterService;
1015
import org.elasticsearch.common.io.stream.BytesStreamOutput;
1116
import org.elasticsearch.license.XPackLicenseState;
17+
import org.elasticsearch.tasks.Task;
1218
import org.elasticsearch.test.ESTestCase;
1319
import org.elasticsearch.transport.TransportService;
1420
import org.elasticsearch.xpack.core.XPackFeatureSet;
1521
import org.elasticsearch.xpack.core.action.XPackUsageFeatureResponse;
1622
import org.elasticsearch.xpack.core.analytics.AnalyticsFeatureSetUsage;
23+
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
1724
import org.junit.Before;
25+
import org.mockito.stubbing.Answer;
26+
27+
import java.util.Collections;
1828

1929
import static org.hamcrest.core.Is.is;
30+
import static org.mockito.Matchers.any;
31+
import static org.mockito.Matchers.eq;
32+
import static org.mockito.Mockito.doAnswer;
2033
import static org.mockito.Mockito.mock;
34+
import static org.mockito.Mockito.times;
35+
import static org.mockito.Mockito.verify;
36+
import static org.mockito.Mockito.verifyNoMoreInteractions;
2137
import static org.mockito.Mockito.when;
2238

2339
public class AnalyticsInfoTransportActionTests extends ESTestCase {
2440

2541
private XPackLicenseState licenseState;
42+
private Task task;
43+
private ClusterService clusterService;
44+
private ClusterName clusterName;
2645

2746
@Before
2847
public void init() {
2948
licenseState = mock(XPackLicenseState.class);
49+
task = mock(Task.class);
50+
when(task.getId()).thenReturn(randomLong());
51+
clusterService = mock(ClusterService.class);
52+
DiscoveryNode discoveryNode = mock(DiscoveryNode.class);
53+
when(discoveryNode.getId()).thenReturn(randomAlphaOfLength(10));
54+
when(clusterService.localNode()).thenReturn(discoveryNode);
55+
clusterName = mock(ClusterName.class);
3056
}
3157

3258
public void testAvailable() throws Exception {
@@ -35,37 +61,59 @@ public void testAvailable() throws Exception {
3561
boolean available = randomBoolean();
3662
when(licenseState.isDataScienceAllowed()).thenReturn(available);
3763
assertThat(featureSet.available(), is(available));
38-
39-
AnalyticsUsageTransportAction usageAction = new AnalyticsUsageTransportAction(mock(TransportService.class), null, null,
40-
mock(ActionFilters.class), null, licenseState);
64+
Client client = mockClient();
65+
AnalyticsUsageTransportAction usageAction = new AnalyticsUsageTransportAction(mock(TransportService.class), clusterService, null,
66+
mock(ActionFilters.class), null, licenseState, client);
4167
PlainActionFuture<XPackUsageFeatureResponse> future = new PlainActionFuture<>();
42-
usageAction.masterOperation(null, null, null, future);
68+
usageAction.masterOperation(task, null, null, future);
4369
XPackFeatureSet.Usage usage = future.get().getUsage();
4470
assertThat(usage.available(), is(available));
4571

4672
BytesStreamOutput out = new BytesStreamOutput();
4773
usage.writeTo(out);
4874
XPackFeatureSet.Usage serializedUsage = new AnalyticsFeatureSetUsage(out.bytes().streamInput());
4975
assertThat(serializedUsage.available(), is(available));
76+
if (available) {
77+
verify(client, times(1)).execute(any(), any(), any());
78+
}
79+
verifyNoMoreInteractions(client);
5080
}
5181

5282
public void testEnabled() throws Exception {
5383
AnalyticsInfoTransportAction featureSet = new AnalyticsInfoTransportAction(
5484
mock(TransportService.class), mock(ActionFilters.class), licenseState);
5585
assertThat(featureSet.enabled(), is(true));
5686
assertTrue(featureSet.enabled());
57-
87+
boolean available = randomBoolean();
88+
when(licenseState.isDataScienceAllowed()).thenReturn(available);
89+
Client client = mockClient();
5890
AnalyticsUsageTransportAction usageAction = new AnalyticsUsageTransportAction(mock(TransportService.class),
59-
null, null, mock(ActionFilters.class), null, licenseState);
91+
clusterService, null, mock(ActionFilters.class), null, licenseState, client);
6092
PlainActionFuture<XPackUsageFeatureResponse> future = new PlainActionFuture<>();
61-
usageAction.masterOperation(null, null, null, future);
93+
usageAction.masterOperation(task, null, null, future);
6294
XPackFeatureSet.Usage usage = future.get().getUsage();
6395
assertTrue(usage.enabled());
6496

6597
BytesStreamOutput out = new BytesStreamOutput();
6698
usage.writeTo(out);
6799
XPackFeatureSet.Usage serializedUsage = new AnalyticsFeatureSetUsage(out.bytes().streamInput());
68100
assertTrue(serializedUsage.enabled());
101+
if (available) {
102+
verify(client, times(1)).execute(any(), any(), any());
103+
}
104+
verifyNoMoreInteractions(client);
105+
}
106+
107+
private Client mockClient() {
108+
Client client = mock(Client.class);
109+
doAnswer((Answer<Void>) invocation -> {
110+
@SuppressWarnings("unchecked")
111+
ActionListener<AnalyticsStatsAction.Response> listener =
112+
(ActionListener<AnalyticsStatsAction.Response>) invocation.getArguments()[2];
113+
listener.onResponse(new AnalyticsStatsAction.Response(clusterName, Collections.emptyList(), Collections.emptyList()));
114+
return null;
115+
}).when(client).execute(eq(AnalyticsStatsAction.INSTANCE), any(), any());
116+
return client;
69117
}
70118

71119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.action;
8+
9+
import org.elasticsearch.Version;
10+
import org.elasticsearch.cluster.node.DiscoveryNode;
11+
import org.elasticsearch.common.io.stream.Writeable;
12+
import org.elasticsearch.test.AbstractWireSerializingTestCase;
13+
import org.elasticsearch.xpack.core.analytics.EnumCounters;
14+
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
15+
16+
import static org.hamcrest.Matchers.equalTo;
17+
18+
public class AnalyticsStatsActionNodeResponseTests extends AbstractWireSerializingTestCase<AnalyticsStatsAction.NodeResponse> {
19+
20+
@Override
21+
protected Writeable.Reader<AnalyticsStatsAction.NodeResponse> instanceReader() {
22+
return AnalyticsStatsAction.NodeResponse::new;
23+
}
24+
25+
@Override
26+
protected AnalyticsStatsAction.NodeResponse createTestInstance() {
27+
String nodeName = randomAlphaOfLength(10);
28+
DiscoveryNode node = new DiscoveryNode(nodeName, buildNewFakeTransportAddress(), Version.CURRENT);
29+
EnumCounters<AnalyticsStatsAction.Item> counters = new EnumCounters<>(AnalyticsStatsAction.Item.class);
30+
for (AnalyticsStatsAction.Item item : AnalyticsStatsAction.Item.values()) {
31+
if (randomBoolean()) {
32+
counters.inc(item, randomLongBetween(0, 1000));
33+
}
34+
}
35+
return new AnalyticsStatsAction.NodeResponse(node, counters);
36+
}
37+
38+
public void testItemEnum() {
39+
int i = 0;
40+
// We rely on the ordinals for serialization, so they shouldn't change between version
41+
assertThat(AnalyticsStatsAction.Item.BOXPLOT.ordinal(), equalTo(i++));
42+
assertThat(AnalyticsStatsAction.Item.CUMULATIVE_CARDINALITY.ordinal(), equalTo(i++));
43+
assertThat(AnalyticsStatsAction.Item.STRING_STATS.ordinal(), equalTo(i++));
44+
assertThat(AnalyticsStatsAction.Item.TOP_METRICS.ordinal(), equalTo(i++));
45+
assertThat(AnalyticsStatsAction.Item.T_TEST.ordinal(), equalTo(i++));
46+
// Please add tests for newly added items here
47+
assertThat(AnalyticsStatsAction.Item.values().length, equalTo(i));
48+
}
49+
}

0 commit comments

Comments
 (0)