Skip to content

Commit 674b834

Browse files
rjernsttvernum
andauthored
Add persistent licensed feature tracking (#76537)
backport of #76476 Licensed feature tracking utilizes the existing license level checks to track when a feature is used. However, some features check the license level at the start of an operation or when enabling a feature, but then the tracking only captures the beginning time. This commit reworks the licensed feature framework to use a new LicensedFeature class which will eventually replace XPackLicenseState.Feature values. There are two LicensedFeature implementations, one for "momentary" features that are tracked just at the moment they are used, and "persistent" features that are considered "on" until the feature is untracked. The usage map of tracked features is cleaned up every hour, and those features that have not been used in the last 24 hours are removed from tracking. Not all features are converted to LicensedFeature yet. Instead, a few features have been converted to demonstrate how it can be done, so that the rest can be done in parallel at a future time. Co-authored-by: Tim Vernum <[email protected]>
1 parent 5ef2a8a commit 674b834

File tree

22 files changed

+562
-195
lines changed

22 files changed

+562
-195
lines changed

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,43 +7,56 @@
77

88
package org.elasticsearch.license;
99

10+
import org.elasticsearch.Version;
1011
import org.elasticsearch.action.ActionResponse;
1112
import org.elasticsearch.common.io.stream.StreamInput;
1213
import org.elasticsearch.common.io.stream.StreamOutput;
1314
import org.elasticsearch.common.io.stream.Writeable;
1415
import org.elasticsearch.common.xcontent.ToXContentObject;
1516
import org.elasticsearch.common.xcontent.XContentBuilder;
17+
import org.elasticsearch.core.Nullable;
1618

1719
import java.io.IOException;
1820
import java.time.Instant;
1921
import java.time.ZoneOffset;
2022
import java.time.ZonedDateTime;
2123
import java.util.Collections;
2224
import java.util.List;
25+
import java.util.Objects;
2326

2427
public class GetFeatureUsageResponse extends ActionResponse implements ToXContentObject {
2528

2629
public static class FeatureUsageInfo implements Writeable {
27-
public final String name;
28-
public final ZonedDateTime lastUsedTime;
30+
private final String name;
31+
private final ZonedDateTime lastUsedTime;
32+
private final String context;
2933
public final String licenseLevel;
3034

31-
public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, String licenseLevel) {
32-
this.name = name;
33-
this.lastUsedTime = lastUsedTime;
34-
this.licenseLevel = licenseLevel;
35+
public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, @Nullable String context, String licenseLevel) {
36+
this.name = Objects.requireNonNull(name, "Feature name may not be null");
37+
this.lastUsedTime = Objects.requireNonNull(lastUsedTime, "Last used time may not be null");
38+
this.context = context;
39+
this.licenseLevel = Objects.requireNonNull(licenseLevel, "License level may not be null");
3540
}
3641

3742
public FeatureUsageInfo(StreamInput in) throws IOException {
3843
this.name = in.readString();
3944
this.lastUsedTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(in.readLong()), ZoneOffset.UTC);
45+
if (in.getVersion().onOrAfter(Version.V_7_15_0)) {
46+
this.context = in.readOptionalString();
47+
} else {
48+
this.context = null;
49+
}
4050
this.licenseLevel = in.readString();
4151
}
4252

4353
@Override
4454
public void writeTo(StreamOutput out) throws IOException {
4555
out.writeString(name);
4656
out.writeLong(lastUsedTime.toEpochSecond());
57+
if (out.getVersion().onOrAfter(Version.V_7_15_0)) {
58+
out.writeOptionalString(this.context);
59+
}
4760
out.writeString(licenseLevel);
4861
}
4962
}
@@ -74,6 +87,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
7487
for (FeatureUsageInfo feature : features) {
7588
builder.startObject();
7689
builder.field("name", feature.name);
90+
builder.field("context", feature.context);
7791
builder.field("last_used", feature.lastUsedTime.toString());
7892
builder.field("license_level", feature.licenseLevel);
7993
builder.endObject();

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest;
3434
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
3535
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
36+
import org.elasticsearch.threadpool.ThreadPool;
3637
import org.elasticsearch.watcher.ResourceWatcherService;
3738
import org.elasticsearch.xpack.core.XPackPlugin;
3839
import org.elasticsearch.xpack.core.XPackSettings;
@@ -131,7 +132,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
131132
private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " +
132133
"please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";
133134

134-
public LicenseService(Settings settings, ClusterService clusterService, Clock clock, Environment env,
135+
public LicenseService(Settings settings, ThreadPool threadPool, ClusterService clusterService, Clock clock, Environment env,
135136
ResourceWatcherService resourceWatcherService, XPackLicenseState licenseState) {
136137
this.settings = settings;
137138
this.clusterService = clusterService;
@@ -144,6 +145,8 @@ public LicenseService(Settings settings, ClusterService clusterService, Clock cl
144145
() -> updateLicenseState(getLicensesMetadata()));
145146
this.scheduler.register(this);
146147
populateExpirationCallbacks();
148+
149+
threadPool.scheduleWithFixedDelay(licenseState::cleanupUsageTracking, TimeValue.timeValueHours(1), ThreadPool.Names.GENERIC);
147150
}
148151

149152
private void logExpirationWarning(long expirationMillis, boolean expired) {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.license;
9+
10+
import java.util.Objects;
11+
12+
/**
13+
* A base class for checking licensed features against the license.
14+
*/
15+
public abstract class LicensedFeature {
16+
17+
/**
18+
* A Momentary feature is one that is tracked at the moment the license is checked.
19+
*/
20+
public static class Momentary extends LicensedFeature {
21+
22+
private Momentary(String name, License.OperationMode minimumOperationMode, boolean needsActive) {
23+
super(name, minimumOperationMode, needsActive);
24+
}
25+
26+
/**
27+
* Checks whether the feature is allowed by the given license state, and
28+
* updates the last time the feature was used.
29+
*/
30+
public boolean check(XPackLicenseState state) {
31+
if (state.isAllowed(this)) {
32+
state.featureUsed(this);
33+
return true;
34+
} else {
35+
return false;
36+
}
37+
}
38+
}
39+
40+
/**
41+
* A Persistent feature is one that is tracked starting when the license is checked, and later may be untracked.
42+
*/
43+
public static class Persistent extends LicensedFeature {
44+
private Persistent(String name, License.OperationMode minimumOperationMode, boolean needsActive) {
45+
super(name, minimumOperationMode, needsActive);
46+
}
47+
48+
/**
49+
* Checks whether the feature is allowed by the given license state, and
50+
* begins tracking the feature as "on" for the given context.
51+
*/
52+
public boolean checkAndStartTracking(XPackLicenseState state, String contextName) {
53+
if (state.isAllowed(this)) {
54+
state.enableUsageTracking(this, contextName);
55+
return true;
56+
} else {
57+
return false;
58+
}
59+
}
60+
61+
/**
62+
* Stop tracking the feature so that the current time will be the last that it was used.
63+
*/
64+
public void stopTracking(XPackLicenseState state, String contextName) {
65+
state.disableUsageTracking(this, contextName);
66+
}
67+
}
68+
69+
final String name;
70+
final License.OperationMode minimumOperationMode;
71+
final boolean needsActive;
72+
73+
public LicensedFeature(String name, License.OperationMode minimumOperationMode, boolean needsActive) {
74+
this.name = name;
75+
this.minimumOperationMode = minimumOperationMode;
76+
this.needsActive = needsActive;
77+
}
78+
79+
/** Create a momentary feature for hte given license level */
80+
public static Momentary momentary(String name, License.OperationMode licenseLevel) {
81+
return new Momentary(name, licenseLevel, true);
82+
}
83+
84+
/** Create a persistent feature for the given license level */
85+
public static Persistent persistent(String name, License.OperationMode licenseLevel) {
86+
return new Persistent(name, licenseLevel, true);
87+
}
88+
89+
/**
90+
* Creates a momentary feature, but one that is lenient as
91+
* to whether the license needs to be active to allow the feature.
92+
*/
93+
@Deprecated
94+
public static Momentary momentaryLenient(String name, License.OperationMode licenseLevel) {
95+
return new Momentary(name, licenseLevel, false);
96+
}
97+
98+
/**
99+
* Creates a persistent feature, but one that is lenient as
100+
* to whether the license needs to be active to allow the feature.
101+
*/
102+
@Deprecated
103+
public static Persistent persistentLenient(String name, License.OperationMode licenseLevel) {
104+
return new Persistent(name, licenseLevel, false);
105+
}
106+
107+
/**
108+
* Returns whether the feature is allowed by the current license
109+
* without affecting feature tracking.
110+
*/
111+
public final boolean checkWithoutTracking(XPackLicenseState state) {
112+
return state.isAllowed(this);
113+
}
114+
115+
@Override
116+
public boolean equals(Object o) {
117+
if (this == o) return true;
118+
if (o == null || getClass() != o.getClass()) return false;
119+
LicensedFeature that = (LicensedFeature) o;
120+
return Objects.equals(name, that.name);
121+
}
122+
123+
@Override
124+
public int hashCode() {
125+
return Objects.hash(name);
126+
}
127+
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.time.ZonedDateTime;
2121
import java.util.ArrayList;
2222
import java.util.List;
23-
import java.util.Locale;
2423
import java.util.Map;
2524

2625
public class TransportGetFeatureUsageAction extends HandledTransportAction<GetFeatureUsageRequest, GetFeatureUsageResponse> {
@@ -40,15 +39,19 @@ public TransportGetFeatureUsageAction(TransportService transportService, ActionF
4039

4140
@Override
4241
protected void doExecute(Task task, GetFeatureUsageRequest request, ActionListener<GetFeatureUsageResponse> listener) {
43-
Map<XPackLicenseState.Feature, Long> featureUsage = licenseState.getLastUsed();
44-
List<GetFeatureUsageResponse.FeatureUsageInfo> usageInfos = new ArrayList<>();
45-
for (Map.Entry<XPackLicenseState.Feature, Long> entry : featureUsage.entrySet()) {
46-
XPackLicenseState.Feature feature = entry.getKey();
47-
String name = feature.name().toLowerCase(Locale.ROOT);
48-
ZonedDateTime lastUsedTime = Instant.ofEpochMilli(entry.getValue()).atZone(ZoneOffset.UTC);
49-
String licenseLevel = feature.minimumOperationMode.name().toLowerCase(Locale.ROOT);
50-
usageInfos.add(new GetFeatureUsageResponse.FeatureUsageInfo(name, lastUsedTime, licenseLevel));
51-
}
42+
Map<XPackLicenseState.FeatureUsage, Long> featureUsage = licenseState.getLastUsed();
43+
List<GetFeatureUsageResponse.FeatureUsageInfo> usageInfos = new ArrayList<>(featureUsage.size());
44+
featureUsage.forEach((usage, lastUsed) -> {
45+
ZonedDateTime lastUsedTime = Instant.ofEpochMilli(lastUsed).atZone(ZoneOffset.UTC);
46+
usageInfos.add(
47+
new GetFeatureUsageResponse.FeatureUsageInfo(
48+
usage.featureName(),
49+
lastUsedTime,
50+
usage.contextName(),
51+
usage.minimumOperationMode().description()
52+
)
53+
);
54+
});
5255
listener.onResponse(new GetFeatureUsageResponse(usageInfos));
5356
}
5457
}

0 commit comments

Comments
 (0)