Skip to content

Commit

Permalink
Add persistent licensed feature tracking (#76537)
Browse files Browse the repository at this point in the history
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 <tim@adjective.org>
  • Loading branch information
rjernst and tvernum committed Aug 16, 2021
1 parent 5ef2a8a commit 674b834
Show file tree
Hide file tree
Showing 22 changed files with 562 additions and 195 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,56 @@

package org.elasticsearch.license;

import org.elasticsearch.Version;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.core.Nullable;

import java.io.IOException;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

public class GetFeatureUsageResponse extends ActionResponse implements ToXContentObject {

public static class FeatureUsageInfo implements Writeable {
public final String name;
public final ZonedDateTime lastUsedTime;
private final String name;
private final ZonedDateTime lastUsedTime;
private final String context;
public final String licenseLevel;

public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, String licenseLevel) {
this.name = name;
this.lastUsedTime = lastUsedTime;
this.licenseLevel = licenseLevel;
public FeatureUsageInfo(String name, ZonedDateTime lastUsedTime, @Nullable String context, String licenseLevel) {
this.name = Objects.requireNonNull(name, "Feature name may not be null");
this.lastUsedTime = Objects.requireNonNull(lastUsedTime, "Last used time may not be null");
this.context = context;
this.licenseLevel = Objects.requireNonNull(licenseLevel, "License level may not be null");
}

public FeatureUsageInfo(StreamInput in) throws IOException {
this.name = in.readString();
this.lastUsedTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(in.readLong()), ZoneOffset.UTC);
if (in.getVersion().onOrAfter(Version.V_7_15_0)) {
this.context = in.readOptionalString();
} else {
this.context = null;
}
this.licenseLevel = in.readString();
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(name);
out.writeLong(lastUsedTime.toEpochSecond());
if (out.getVersion().onOrAfter(Version.V_7_15_0)) {
out.writeOptionalString(this.context);
}
out.writeString(licenseLevel);
}
}
Expand Down Expand Up @@ -74,6 +87,7 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
for (FeatureUsageInfo feature : features) {
builder.startObject();
builder.field("name", feature.name);
builder.field("context", feature.context);
builder.field("last_used", feature.lastUsedTime.toString());
builder.field("license_level", feature.licenseLevel);
builder.endObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.elasticsearch.protocol.xpack.license.DeleteLicenseRequest;
import org.elasticsearch.protocol.xpack.license.LicensesStatus;
import org.elasticsearch.protocol.xpack.license.PutLicenseResponse;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.XPackSettings;
Expand Down Expand Up @@ -131,7 +132,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
private static final String ACKNOWLEDGEMENT_HEADER = "This license update requires acknowledgement. To acknowledge the license, " +
"please read the following messages and update the license again, this time with the \"acknowledge=true\" parameter:";

public LicenseService(Settings settings, ClusterService clusterService, Clock clock, Environment env,
public LicenseService(Settings settings, ThreadPool threadPool, ClusterService clusterService, Clock clock, Environment env,
ResourceWatcherService resourceWatcherService, XPackLicenseState licenseState) {
this.settings = settings;
this.clusterService = clusterService;
Expand All @@ -144,6 +145,8 @@ public LicenseService(Settings settings, ClusterService clusterService, Clock cl
() -> updateLicenseState(getLicensesMetadata()));
this.scheduler.register(this);
populateExpirationCallbacks();

threadPool.scheduleWithFixedDelay(licenseState::cleanupUsageTracking, TimeValue.timeValueHours(1), ThreadPool.Names.GENERIC);
}

private void logExpirationWarning(long expirationMillis, boolean expired) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.license;

import java.util.Objects;

/**
* A base class for checking licensed features against the license.
*/
public abstract class LicensedFeature {

/**
* A Momentary feature is one that is tracked at the moment the license is checked.
*/
public static class Momentary extends LicensedFeature {

private Momentary(String name, License.OperationMode minimumOperationMode, boolean needsActive) {
super(name, minimumOperationMode, needsActive);
}

/**
* Checks whether the feature is allowed by the given license state, and
* updates the last time the feature was used.
*/
public boolean check(XPackLicenseState state) {
if (state.isAllowed(this)) {
state.featureUsed(this);
return true;
} else {
return false;
}
}
}

/**
* A Persistent feature is one that is tracked starting when the license is checked, and later may be untracked.
*/
public static class Persistent extends LicensedFeature {
private Persistent(String name, License.OperationMode minimumOperationMode, boolean needsActive) {
super(name, minimumOperationMode, needsActive);
}

/**
* Checks whether the feature is allowed by the given license state, and
* begins tracking the feature as "on" for the given context.
*/
public boolean checkAndStartTracking(XPackLicenseState state, String contextName) {
if (state.isAllowed(this)) {
state.enableUsageTracking(this, contextName);
return true;
} else {
return false;
}
}

/**
* Stop tracking the feature so that the current time will be the last that it was used.
*/
public void stopTracking(XPackLicenseState state, String contextName) {
state.disableUsageTracking(this, contextName);
}
}

final String name;
final License.OperationMode minimumOperationMode;
final boolean needsActive;

public LicensedFeature(String name, License.OperationMode minimumOperationMode, boolean needsActive) {
this.name = name;
this.minimumOperationMode = minimumOperationMode;
this.needsActive = needsActive;
}

/** Create a momentary feature for hte given license level */
public static Momentary momentary(String name, License.OperationMode licenseLevel) {
return new Momentary(name, licenseLevel, true);
}

/** Create a persistent feature for the given license level */
public static Persistent persistent(String name, License.OperationMode licenseLevel) {
return new Persistent(name, licenseLevel, true);
}

/**
* Creates a momentary feature, but one that is lenient as
* to whether the license needs to be active to allow the feature.
*/
@Deprecated
public static Momentary momentaryLenient(String name, License.OperationMode licenseLevel) {
return new Momentary(name, licenseLevel, false);
}

/**
* Creates a persistent feature, but one that is lenient as
* to whether the license needs to be active to allow the feature.
*/
@Deprecated
public static Persistent persistentLenient(String name, License.OperationMode licenseLevel) {
return new Persistent(name, licenseLevel, false);
}

/**
* Returns whether the feature is allowed by the current license
* without affecting feature tracking.
*/
public final boolean checkWithoutTracking(XPackLicenseState state) {
return state.isAllowed(this);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LicensedFeature that = (LicensedFeature) o;
return Objects.equals(name, that.name);
}

@Override
public int hashCode() {
return Objects.hash(name);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

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

@Override
protected void doExecute(Task task, GetFeatureUsageRequest request, ActionListener<GetFeatureUsageResponse> listener) {
Map<XPackLicenseState.Feature, Long> featureUsage = licenseState.getLastUsed();
List<GetFeatureUsageResponse.FeatureUsageInfo> usageInfos = new ArrayList<>();
for (Map.Entry<XPackLicenseState.Feature, Long> entry : featureUsage.entrySet()) {
XPackLicenseState.Feature feature = entry.getKey();
String name = feature.name().toLowerCase(Locale.ROOT);
ZonedDateTime lastUsedTime = Instant.ofEpochMilli(entry.getValue()).atZone(ZoneOffset.UTC);
String licenseLevel = feature.minimumOperationMode.name().toLowerCase(Locale.ROOT);
usageInfos.add(new GetFeatureUsageResponse.FeatureUsageInfo(name, lastUsedTime, licenseLevel));
}
Map<XPackLicenseState.FeatureUsage, Long> featureUsage = licenseState.getLastUsed();
List<GetFeatureUsageResponse.FeatureUsageInfo> usageInfos = new ArrayList<>(featureUsage.size());
featureUsage.forEach((usage, lastUsed) -> {
ZonedDateTime lastUsedTime = Instant.ofEpochMilli(lastUsed).atZone(ZoneOffset.UTC);
usageInfos.add(
new GetFeatureUsageResponse.FeatureUsageInfo(
usage.featureName(),
lastUsedTime,
usage.contextName(),
usage.minimumOperationMode().description()
)
);
});
listener.onResponse(new GetFeatureUsageResponse(usageInfos));
}
}

0 comments on commit 674b834

Please sign in to comment.