Skip to content

Commit 9c487ad

Browse files
committed
Add analytics plugin usage stats to _xpack/usage (elastic#54911)
Adds analytics plugin usage stats to _xpack/usage. Closes elastic#54847
1 parent 3fbd8b3 commit 9c487ad

File tree

11 files changed

+608
-123
lines changed

11 files changed

+608
-123
lines changed

docs/reference/rest-api/usage.asciidoc

Lines changed: 3 additions & 3 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]]
@@ -291,9 +291,9 @@ GET /_xpack/usage
291291
// 1. Handling eql, which is disabled by default on release builds and enabled
292292
// everywhere else during the initial implementation phase until its release
293293
// 2. Ignore the contents of the `ilm` and `slm` objects because they don't know
294-
// all of the policies that will be in them.
294+
// all of the policies that will be in them.
295295
// 3. Ignore the contents of the `analytics` object because it might contain
296296
// additional stats
297297
// 4. All of the numbers and strings on the right hand side of *every* field in
298298
// the response are ignored. So we're really only asserting things about the
299-
// the shape of this response, not the values in it.
299+
// the shape of this response, not the values in it.

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
@@ -74,7 +74,7 @@ public List<PipelineAggregationSpec> getPipelineAggregations() {
7474
CumulativeCardinalityPipelineAggregationBuilder.NAME,
7575
CumulativeCardinalityPipelineAggregationBuilder::new,
7676
CumulativeCardinalityPipelineAggregator::new,
77-
usage.track(AnalyticsUsage.Item.CUMULATIVE_CARDINALITY,
77+
usage.track(AnalyticsStatsAction.Item.CUMULATIVE_CARDINALITY,
7878
checkLicense(CumulativeCardinalityPipelineAggregationBuilder.PARSER)))
7979
);
8080
}
@@ -85,22 +85,22 @@ public List<AggregationSpec> getAggregations() {
8585
new AggregationSpec(
8686
StringStatsAggregationBuilder.NAME,
8787
StringStatsAggregationBuilder::new,
88-
usage.track(AnalyticsUsage.Item.STRING_STATS, checkLicense(StringStatsAggregationBuilder.PARSER)))
88+
usage.track(AnalyticsStatsAction.Item.STRING_STATS, checkLicense(StringStatsAggregationBuilder.PARSER)))
8989
.addResultReader(InternalStringStats::new),
9090
new AggregationSpec(
9191
BoxplotAggregationBuilder.NAME,
9292
BoxplotAggregationBuilder::new,
93-
usage.track(AnalyticsUsage.Item.BOXPLOT, checkLicense(BoxplotAggregationBuilder.PARSER)))
93+
usage.track(AnalyticsStatsAction.Item.BOXPLOT, checkLicense(BoxplotAggregationBuilder.PARSER)))
9494
.addResultReader(InternalBoxplot::new),
9595
new AggregationSpec(
9696
TopMetricsAggregationBuilder.NAME,
9797
TopMetricsAggregationBuilder::new,
98-
usage.track(AnalyticsUsage.Item.TOP_METRICS, checkLicense(TopMetricsAggregationBuilder.PARSER)))
98+
usage.track(AnalyticsStatsAction.Item.TOP_METRICS, checkLicense(TopMetricsAggregationBuilder.PARSER)))
9999
.addResultReader(InternalTopMetrics::new),
100100
new AggregationSpec(
101101
TTestAggregationBuilder.NAME,
102102
TTestAggregationBuilder::new,
103-
usage.track(AnalyticsUsage.Item.T_TEST, checkLicense(TTestAggregationBuilder.PARSER)))
103+
usage.track(AnalyticsStatsAction.Item.T_TEST, checkLicense(TTestAggregationBuilder.PARSER)))
104104
.addResultReader(InternalTTest::new)
105105
);
106106
}
@@ -138,7 +138,7 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
138138
ResourceWatcherService resourceWatcherService, ScriptService scriptService, NamedXContentRegistry xContentRegistry,
139139
Environment environment, NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry,
140140
IndexNameExpressionResolver indexNameExpressionResolver) {
141-
return singletonList(new AnalyticsUsage());
141+
return singletonList(usage);
142142
}
143143

144144
@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
}
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+
}

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.threadpool.ThreadPool;
2323
import org.elasticsearch.transport.TransportService;
2424
import org.elasticsearch.xpack.analytics.AnalyticsUsage;
25+
import org.elasticsearch.xpack.core.analytics.AnalyticsFeatureSetUsage;
2526
import org.elasticsearch.xpack.core.analytics.action.AnalyticsStatsAction;
2627

2728
import java.io.IOException;
@@ -33,6 +34,7 @@
3334
import static java.util.Collections.emptyList;
3435
import static java.util.stream.Collectors.toList;
3536
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
37+
import static org.elasticsearch.xpack.analytics.action.AnalyticsUsageTransportAction.usageFeatureResponse;
3638
import static org.hamcrest.Matchers.equalTo;
3739
import static org.mockito.Mockito.mock;
3840
import static org.mockito.Mockito.when;
@@ -58,20 +60,18 @@ public TransportAnalyticsStatsAction action(AnalyticsUsage usage) {
5860
}
5961

6062
public void test() throws IOException {
61-
for (AnalyticsUsage.Item item : AnalyticsUsage.Item.values()) {
63+
for (AnalyticsStatsAction.Item item : AnalyticsStatsAction.Item.values()) {
6264
AnalyticsUsage realUsage = new AnalyticsUsage();
6365
AnalyticsUsage emptyUsage = new AnalyticsUsage();
6466
ContextParser<Void, Void> parser = realUsage.track(item, (p, c) -> c);
6567
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+
assertThat(unused.evaluate("stats." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(0));
6869
int count = between(1, 10000);
6970
for (int i = 0; i < count; i++) {
7071
assertNull(parser.parse(null, null));
7172
}
7273
ObjectPath used = run(realUsage, emptyUsage);
73-
assertThat(item.name(), used.evaluate("stats.0." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(count));
74-
assertThat(item.name(), used.evaluate("stats.1." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(0));
74+
assertThat(item.name(), used.evaluate("stats." + item.name().toLowerCase(Locale.ROOT) + "_usage"), equalTo(count));
7575
}
7676
}
7777

@@ -83,11 +83,9 @@ private ObjectPath run(AnalyticsUsage... nodeUsages) throws IOException {
8383
AnalyticsStatsAction.Response response = new AnalyticsStatsAction.Response(
8484
new ClusterName("cluster_name"), nodeResponses, emptyList());
8585

86+
AnalyticsFeatureSetUsage usage = usageFeatureResponse(true, true, response);
8687
try (XContentBuilder builder = jsonBuilder()) {
87-
builder.startObject();
88-
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
89-
builder.endObject();
90-
88+
usage.toXContent(builder, ToXContent.EMPTY_PARAMS);
9189
return ObjectPath.createFromXContent(JsonXContent.jsonXContent, BytesReference.bytes(builder));
9290
}
9391
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/XPackUsageResponse.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ public void writeTo(StreamOutput out) throws IOException {
4343
}
4444
}
4545

46-
}
46+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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.core.analytics;
8+
9+
import org.elasticsearch.common.io.stream.StreamInput;
10+
import org.elasticsearch.common.io.stream.StreamOutput;
11+
import org.elasticsearch.common.io.stream.Writeable;
12+
13+
import java.io.IOException;
14+
import java.util.Arrays;
15+
import java.util.HashMap;
16+
import java.util.List;
17+
import java.util.Locale;
18+
import java.util.Map;
19+
import java.util.concurrent.atomic.AtomicLongArray;
20+
21+
/**
22+
* Utility class similar to org.elasticsearch.xpack.core.watcher.common.stats.Counters, but it is using Enum instead
23+
* of string to identify the counter. The serialization happens using enum ordinals similar to
24+
* {@link StreamOutput#writeEnum(Enum)}, which means that ordinal for existing enums should remain the same for backward
25+
* and forward compatibility of the serialization protocol.
26+
*/
27+
public class EnumCounters<E extends Enum<E>> implements Writeable {
28+
private final AtomicLongArray counters;
29+
private final E[] enums;
30+
31+
public EnumCounters(Class<E> enumClass) {
32+
counters = new AtomicLongArray(enumClass.getEnumConstants().length);
33+
enums = enumClass.getEnumConstants();
34+
}
35+
36+
public EnumCounters(StreamInput in, Class<E> enumClass) throws IOException {
37+
int size = in.readVInt();
38+
enums = enumClass.getEnumConstants();
39+
long[] vals = new long[enums.length];
40+
for (int i = 0; i < size; i++) {
41+
long val = in.readVLong();
42+
if (i < vals.length) {
43+
vals[i] = val;
44+
}
45+
}
46+
counters = new AtomicLongArray(vals);
47+
}
48+
49+
@Override
50+
public void writeTo(StreamOutput out) throws IOException {
51+
out.writeVInt(counters.length());
52+
for (int i = 0; i < counters.length(); i++) {
53+
out.writeVLong(counters.get(i));
54+
}
55+
}
56+
57+
public void set(E name) {
58+
counters.set(name.ordinal(), 0);
59+
}
60+
61+
public void inc(E name) {
62+
counters.incrementAndGet(name.ordinal());
63+
}
64+
65+
public void inc(E name, long count) {
66+
counters.addAndGet(name.ordinal(), count);
67+
}
68+
69+
public long get(E name) {
70+
return counters.get(name.ordinal());
71+
}
72+
73+
public long size() {
74+
return counters.length();
75+
}
76+
77+
public boolean hasCounters() {
78+
return size() > 0;
79+
}
80+
81+
public Map<String, Object> toMap() {
82+
Map<String, Object> map = new HashMap<>();
83+
for (E e : enums) {
84+
map.put(e.name().toLowerCase(Locale.ROOT), counters.get(e.ordinal()));
85+
}
86+
return map;
87+
}
88+
89+
public static <E extends Enum<E>> EnumCounters<E> merge(Class<E> enumClass, List<EnumCounters<E>> counters) {
90+
EnumCounters<E> result = new EnumCounters<>(enumClass);
91+
E[] enums = enumClass.getEnumConstants();
92+
for (EnumCounters<E> c : counters) {
93+
for (E e : enums) {
94+
result.inc(e, c.get(e));
95+
}
96+
}
97+
return result;
98+
}
99+
100+
@Override
101+
public boolean equals(Object o) {
102+
if (this == o) return true;
103+
if (o == null || getClass() != o.getClass()) return false;
104+
EnumCounters<?> that = (EnumCounters<?>) o;
105+
return Arrays.equals(toArray(), that.toArray()) &&
106+
Arrays.equals(enums, that.enums);
107+
}
108+
109+
@Override
110+
public int hashCode() {
111+
int result = Arrays.hashCode(toArray());
112+
result = 31 * result + Arrays.hashCode(enums);
113+
return result;
114+
}
115+
116+
@Override
117+
public String toString() {
118+
StringBuilder buf = new StringBuilder("[");
119+
boolean first = true;
120+
for (E e : enums) {
121+
buf.append(e.name().toLowerCase(Locale.ROOT)).append(": ").append(get(e));
122+
if (first) {
123+
buf.append(", ");
124+
first = false;
125+
}
126+
}
127+
buf.append("]");
128+
return buf.toString();
129+
}
130+
131+
private long[] toArray() {
132+
long[] res = new long[enums.length];
133+
for (int i = 0; i < res.length; i++) {
134+
res[i] = counters.get(i);
135+
}
136+
return res;
137+
}
138+
}

0 commit comments

Comments
 (0)