Skip to content

Commit 5d6a2dd

Browse files
authored
Add license feature usage api (#59342)
This commit adds a new api to track when gold+ features are used within x-pack. The tracking is done internally whenever a feature is checked against the current license. The output of the api is a list of each used feature, which includes the name, license level, and last time it was used. In addition to a unit test for the tracking, a rest test is added which ensures starting up a default configured node does not result in any features registering as used. There are a couple features which currently do not work well with the tracking, as they are checked in a manner that makes them look always used. Those features will be fixed in followups, and in this PR they are omitted from the feature usage output.
1 parent 292f207 commit 5d6a2dd

File tree

33 files changed

+536
-123
lines changed

33 files changed

+536
-123
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common;
21+
22+
import java.util.function.Supplier;
23+
24+
public class MemoizedSupplier<T> implements Supplier<T> {
25+
private Supplier<T> supplier;
26+
private T value;
27+
28+
public MemoizedSupplier(Supplier<T> supplier) {
29+
this.supplier = supplier;
30+
}
31+
32+
@Override
33+
public T get() {
34+
if (supplier != null) {
35+
value = supplier.get();
36+
supplier = null;
37+
}
38+
return value;
39+
}
40+
}

x-pack/plugin/core/build.gradle

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import java.nio.file.Paths
77
apply plugin: 'elasticsearch.esplugin'
88
apply plugin: 'elasticsearch.publish'
99
apply plugin: 'elasticsearch.internal-cluster-test'
10+
apply plugin: 'elasticsearch.yaml-rest-test'
1011

1112
archivesBaseName = 'x-pack-core'
1213

@@ -57,6 +58,8 @@ dependencies {
5758
transitive = false
5859
}
5960

61+
yamlRestTestImplementation project(':x-pack:plugin:core')
62+
6063
}
6164

6265
ext.expansions = [
@@ -143,8 +146,19 @@ thirdPartyAudit.ignoreMissingClasses(
143146
'javax.servlet.ServletContextListener'
144147
)
145148

146-
// xpack modules are installed in real clusters as the meta plugin, so
147-
// installing them as individual plugins for integ tests doesn't make sense,
148-
// so we disable integ tests
149-
integTest.enabled = false
149+
restResources {
150+
restApi {
151+
includeCore '*'
152+
}
153+
}
154+
155+
testClusters.yamlRestTest {
156+
testDistribution = 'default'
157+
setting 'xpack.security.enabled', 'true'
158+
setting 'xpack.license.self_generated.type', 'trial'
159+
keystore 'bootstrap.password', 'x-pack-test-password'
160+
user username: "x_pack_rest_user", password: "x-pack-test-password"
161+
}
162+
163+
testingConventions.enabled = false
150164

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.license;
8+
9+
import org.elasticsearch.action.ActionRequest;
10+
import org.elasticsearch.action.ActionRequestValidationException;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
13+
import java.io.IOException;
14+
15+
public class GetFeatureUsageRequest extends ActionRequest {
16+
17+
public GetFeatureUsageRequest() {}
18+
19+
public GetFeatureUsageRequest(StreamInput in) throws IOException {
20+
super(in);
21+
}
22+
23+
@Override
24+
public ActionRequestValidationException validate() {
25+
return null;
26+
}
27+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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.license;
8+
9+
import org.elasticsearch.action.ActionResponse;
10+
import org.elasticsearch.common.io.stream.StreamInput;
11+
import org.elasticsearch.common.io.stream.StreamOutput;
12+
import org.elasticsearch.common.io.stream.Writeable;
13+
import org.elasticsearch.common.xcontent.ToXContentObject;
14+
import org.elasticsearch.common.xcontent.XContentBuilder;
15+
16+
import java.io.IOException;
17+
import java.time.Instant;
18+
import java.time.ZoneOffset;
19+
import java.time.ZonedDateTime;
20+
import java.util.Collections;
21+
import java.util.List;
22+
23+
public class GetFeatureUsageResponse extends ActionResponse implements ToXContentObject {
24+
25+
public static class FeatureUsageInfo implements Writeable {
26+
public final String name;
27+
public final ZonedDateTime lastUsedTime;
28+
public final String licenseLevel;
29+
30+
public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, String licenseLevel) {
31+
this.name = name;
32+
this.lastUsedTime = lastUsedTime;
33+
this.licenseLevel = licenseLevel;
34+
}
35+
36+
public FeatureUsageInfo(StreamInput in) throws IOException {
37+
this.name = in.readString();
38+
this.lastUsedTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(in.readLong()), ZoneOffset.UTC);
39+
this.licenseLevel = in.readString();
40+
}
41+
42+
@Override
43+
public void writeTo(StreamOutput out) throws IOException {
44+
out.writeString(name);
45+
out.writeLong(lastUsedTime.toEpochSecond());
46+
out.writeString(licenseLevel);
47+
}
48+
}
49+
50+
private List<FeatureUsageInfo> features;
51+
52+
public GetFeatureUsageResponse(List<FeatureUsageInfo> features) {
53+
this.features = Collections.unmodifiableList(features);
54+
}
55+
56+
public GetFeatureUsageResponse(StreamInput in) throws IOException {
57+
this.features = in.readList(FeatureUsageInfo::new);
58+
}
59+
60+
public List<FeatureUsageInfo> getFeatures() {
61+
return features;
62+
}
63+
64+
@Override
65+
public void writeTo(StreamOutput out) throws IOException {
66+
out.writeList(features);
67+
}
68+
69+
@Override
70+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
71+
builder.startObject();
72+
builder.startArray("features");
73+
for (FeatureUsageInfo feature : features) {
74+
builder.startObject();
75+
builder.field("name", feature.name);
76+
builder.field("last_used", feature.lastUsedTime.toString());
77+
builder.field("license_level", feature.licenseLevel);
78+
builder.endObject();
79+
}
80+
builder.endArray();
81+
builder.endObject();
82+
return builder;
83+
}
84+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/license/Licensing.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public Licensing(Settings settings) {
6666
new ActionHandler<>(PostStartTrialAction.INSTANCE, TransportPostStartTrialAction.class),
6767
new ActionHandler<>(GetTrialStatusAction.INSTANCE, TransportGetTrialStatusAction.class),
6868
new ActionHandler<>(PostStartBasicAction.INSTANCE, TransportPostStartBasicAction.class),
69-
new ActionHandler<>(GetBasicStatusAction.INSTANCE, TransportGetBasicStatusAction.class));
69+
new ActionHandler<>(GetBasicStatusAction.INSTANCE, TransportGetBasicStatusAction.class),
70+
new ActionHandler<>(TransportGetFeatureUsageAction.TYPE, TransportGetFeatureUsageAction.class));
7071
}
7172

7273
@Override
@@ -81,6 +82,7 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
8182
handlers.add(new RestGetBasicStatus());
8283
handlers.add(new RestPostStartTrialLicense());
8384
handlers.add(new RestPostStartBasicLicense());
85+
handlers.add(new RestGetFeatureUsageAction());
8486
return handlers;
8587
}
8688

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.license;
8+
9+
import org.elasticsearch.client.node.NodeClient;
10+
import org.elasticsearch.rest.BaseRestHandler;
11+
import org.elasticsearch.rest.RestRequest;
12+
import org.elasticsearch.rest.action.RestToXContentListener;
13+
14+
import java.io.IOException;
15+
import java.util.List;
16+
17+
import static org.elasticsearch.rest.RestRequest.Method.GET;
18+
19+
public class RestGetFeatureUsageAction extends BaseRestHandler {
20+
21+
@Override
22+
public String getName() {
23+
return "get_feature_usage";
24+
}
25+
26+
@Override
27+
public List<Route> routes() {
28+
return List.of(new Route(GET, "/_license/feature_usage"));
29+
}
30+
31+
@Override
32+
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
33+
return channel -> client.execute(TransportGetFeatureUsageAction.TYPE, new GetFeatureUsageRequest(),
34+
new RestToXContentListener<>(channel));
35+
}
36+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.license;
8+
9+
import org.elasticsearch.action.ActionListener;
10+
import org.elasticsearch.action.ActionType;
11+
import org.elasticsearch.action.support.ActionFilters;
12+
import org.elasticsearch.action.support.HandledTransportAction;
13+
import org.elasticsearch.common.inject.Inject;
14+
import org.elasticsearch.tasks.Task;
15+
import org.elasticsearch.transport.TransportService;
16+
17+
import java.time.Instant;
18+
import java.time.ZoneOffset;
19+
import java.time.ZonedDateTime;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.Locale;
23+
import java.util.Map;
24+
25+
public class TransportGetFeatureUsageAction extends HandledTransportAction<GetFeatureUsageRequest, GetFeatureUsageResponse> {
26+
27+
public static final ActionType<GetFeatureUsageResponse> TYPE =
28+
new ActionType<>("cluster:admin/xpack/license/feature_usage", GetFeatureUsageResponse::new);
29+
30+
private final XPackLicenseState licenseState;
31+
32+
@Inject
33+
public TransportGetFeatureUsageAction(TransportService transportService, ActionFilters actionFilters,
34+
XPackLicenseState licenseState) {
35+
super(TYPE.name(), transportService, actionFilters, GetFeatureUsageRequest::new);
36+
this.licenseState = licenseState;
37+
}
38+
39+
40+
@Override
41+
protected void doExecute(Task task, GetFeatureUsageRequest request, ActionListener<GetFeatureUsageResponse> listener) {
42+
Map<XPackLicenseState.Feature, Long> featureUsage = licenseState.getLastUsed();
43+
List<GetFeatureUsageResponse.FeatureUsageInfo> usageInfos = new ArrayList<>();
44+
for (var entry : featureUsage.entrySet()) {
45+
XPackLicenseState.Feature feature = entry.getKey();
46+
String name = feature.name().toLowerCase(Locale.ROOT);
47+
ZonedDateTime lastUsedTime = Instant.ofEpochMilli(entry.getValue()).atZone(ZoneOffset.UTC);
48+
String licenseLevel = feature.minimumOperationMode.name().toLowerCase(Locale.ROOT);
49+
usageInfos.add(new GetFeatureUsageResponse.FeatureUsageInfo(name, lastUsedTime, licenseLevel));
50+
}
51+
listener.onResponse(new GetFeatureUsageResponse(usageInfos));
52+
}
53+
}

0 commit comments

Comments
 (0)