Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust SyntheticSourceLicenseService #116647

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b66645f
Adjust SyntheticSourceLicenseService
martijnvg Nov 12, 2024
73e96f2
use one licenced feature for gold and enterprise
martijnvg Nov 18, 2024
452b0d9
Allow cutoff date to be configured via system property.
martijnvg Nov 18, 2024
3d5abfe
Only allow legacy license logic for GA-ed use cases.
martijnvg Nov 18, 2024
83d19cd
Merge remote-tracking branch 'es/main' into synthetic_source_gold_pla…
martijnvg Nov 18, 2024
4270f88
Merge remote-tracking branch 'es/main' into synthetic_source_gold_pla…
martijnvg Nov 19, 2024
5bc5ca5
fix npe
martijnvg Nov 19, 2024
bed3e78
Merge remote-tracking branch 'es/main' into synthetic_source_gold_pla…
martijnvg Nov 21, 2024
9b900bb
more unit tests for SyntheticSourceLicenseService and update default …
martijnvg Nov 21, 2024
5ead140
more tests for SyntheticSourceIndexSettingsProvider
martijnvg Nov 21, 2024
cb62a4c
added an integration level test
martijnvg Nov 21, 2024
4856939
Merge remote-tracking branch 'es/main' into synthetic_source_gold_pla…
martijnvg Nov 21, 2024
5285645
refactored integration to extend from AbstractLicensesIntegrationTest…
martijnvg Nov 21, 2024
ae4276f
more explicit name
martijnvg Nov 21, 2024
17af39a
addressed code review feedback.
martijnvg Nov 21, 2024
33a8e59
verifyNoInteractions
martijnvg Nov 21, 2024
53293bc
use more widen patterns
martijnvg Nov 21, 2024
bd21333
update comment
martijnvg Nov 21, 2024
88fa30d
removed unused method
martijnvg Nov 21, 2024
cd410cd
Merge remote-tracking branch 'es/main' into synthetic_source_gold_pla…
martijnvg Nov 25, 2024
f020aad
simplify code
martijnvg Nov 25, 2024
5ac8f74
beef up testing
martijnvg Nov 25, 2024
657a64a
remove MAX_CUTOFF_DATE
martijnvg Nov 25, 2024
77b0c79
extra test
martijnvg Nov 25, 2024
2f19b01
Merge remote-tracking branch 'es/main' into synthetic_source_gold_pla…
martijnvg Nov 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexSettingProvider;
import org.elasticsearch.license.LicenseService;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.xpack.core.XPackPlugin;
Expand Down Expand Up @@ -46,7 +48,8 @@ public LogsDBPlugin(Settings settings) {

@Override
public Collection<?> createComponents(PluginServices services) {
licenseService.setLicenseState(XPackPlugin.getSharedLicenseState());
licenseService.setLicenseService(getLicenseService());
licenseService.setLicenseState(getLicenseState());
var clusterSettings = services.clusterService().getClusterSettings();
// The `cluster.logsdb.enabled` setting is registered by this plugin, but its value may be updated by other plugins
// before this plugin registers its settings update consumer below. This means we might miss updates that occurred earlier.
Expand Down Expand Up @@ -88,4 +91,12 @@ public List<Setting<?>> getSettings() {
actions.add(new ActionPlugin.ActionHandler<>(XPackInfoFeatureAction.LOGSDB, LogsDBInfoTransportAction.class));
return actions;
}

protected XPackLicenseState getLicenseState() {
return XPackPlugin.getSharedLicenseState();
}

protected LicenseService getLicenseService() {
return XPackPlugin.getSharedLicenseService();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,13 @@ public Settings getAdditionalIndexSettings(
// This index name is used when validating component and index templates, we should skip this check in that case.
// (See MetadataIndexTemplateService#validateIndexTemplateV2(...) method)
boolean isTemplateValidation = "validate-index-name".equals(indexName);
boolean legacyLicensedUsageOfSyntheticSourceAllowed = isLegacyLicensedUsageOfSyntheticSourceAllowed(
templateIndexMode,
indexName,
dataStreamName
);
if (newIndexHasSyntheticSourceUsage(indexName, templateIndexMode, indexTemplateAndCreateRequestSettings, combinedTemplateMappings)
&& syntheticSourceLicenseService.fallbackToStoredSource(isTemplateValidation)) {
&& syntheticSourceLicenseService.fallbackToStoredSource(isTemplateValidation, legacyLicensedUsageOfSyntheticSourceAllowed)) {
LOGGER.debug("creation of index [{}] with synthetic source without it being allowed", indexName);
return Settings.builder()
.put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), SourceFieldMapper.Mode.STORED.toString())
Expand Down Expand Up @@ -167,4 +172,29 @@ private IndexMetadata buildIndexMetadataForMapperService(
tmpIndexMetadata.settings(finalResolvedSettings);
return tmpIndexMetadata.build();
}

/**
* The GA-ed use cases in which synthetic source usage is allowed with gold or platinum license.
*/
boolean isLegacyLicensedUsageOfSyntheticSourceAllowed(IndexMode templateIndexMode, String indexName, String dataStreamName) {
if (templateIndexMode == IndexMode.TIME_SERIES) {
return true;
}

// To allow the following patterns: profiling-metrics and profiling-events
if (dataStreamName != null && dataStreamName.startsWith("profiling-")) {
return true;
}
// To allow the following patterns: .profiling-sq-executables, .profiling-sq-leafframes and .profiling-stacktraces
if (indexName.startsWith(".profiling-")) {
return true;
}
Comment on lines +184 to +191
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danielmitterdorfer Double checking, is the pattern sufficient in selecting the profiling data streams and indices that currently use synthetic source? Synthetic source will require an enterprise license from 8.17.0, but for profiling's synthetic source usage we will allow gold/platinum license for a little longer.

// To allow the following patterns: metrics-apm.transaction.*, metrics-apm.service_transaction.*, metrics-apm.service_summary.*,
martijnvg marked this conversation as resolved.
Show resolved Hide resolved
// metrics-apm.service_destination.*, "metrics-apm.internal-* and metrics-apm.app.*
if (dataStreamName != null && dataStreamName.startsWith("metrics-apm.")) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@felixbarny Double checking, is the pattern sufficient in selecting the apm data streams that currently use synthetic source? Synthetic source will require an enterprise license from 8.17.0, but for apm's synthetic source usage we will allow gold/platinum license for a little longer.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the pattern looks good. For reference, all the APM templates are in Elasticsearch: https://github.com/elastic/elasticsearch/tree/main/x-pack/plugin/apm-data/src/main/resources/index-templates

return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,30 @@

package org.elasticsearch.xpack.logsdb;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.License;
import org.elasticsearch.license.LicenseService;
import org.elasticsearch.license.LicensedFeature;
import org.elasticsearch.license.XPackLicenseState;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

/**
* Determines based on license and fallback setting whether synthetic source usages should fallback to stored source.
*/
final class SyntheticSourceLicenseService {

private static final String MAPPINGS_FEATURE_FAMILY = "mappings";
static final String MAPPINGS_FEATURE_FAMILY = "mappings";
// You can only override this property if you received explicit approval from Elastic.
private static final String CUTOFF_DATE_SYS_PROP_NAME =
"es.mapping.synthetic_source_fallback_to_stored_source.cutoff_date_restricted_override";
private static final Logger LOGGER = LogManager.getLogger(SyntheticSourceLicenseService.class);
static final long DEFAULT_CUTOFF_DATE = LocalDateTime.of(2024, 12, 12, 0, 0).toInstant(ZoneOffset.UTC).toEpochMilli();

/**
* A setting that determines whether source mode should always be stored source. Regardless of licence.
Expand All @@ -30,39 +42,98 @@ final class SyntheticSourceLicenseService {
Setting.Property.Dynamic
);

private static final LicensedFeature.Momentary SYNTHETIC_SOURCE_FEATURE = LicensedFeature.momentary(
static final LicensedFeature.Momentary SYNTHETIC_SOURCE_FEATURE = LicensedFeature.momentary(
MAPPINGS_FEATURE_FAMILY,
"synthetic-source",
License.OperationMode.ENTERPRISE
);

static final LicensedFeature.Momentary SYNTHETIC_SOURCE_FEATURE_LEGACY = LicensedFeature.momentary(
MAPPINGS_FEATURE_FAMILY,
"synthetic-source-legacy",
License.OperationMode.GOLD
);

private final long cutoffDate;
private LicenseService licenseService;
private XPackLicenseState licenseState;
private volatile boolean syntheticSourceFallback;

SyntheticSourceLicenseService(Settings settings) {
syntheticSourceFallback = FALLBACK_SETTING.get(settings);
this(settings, System.getProperty(CUTOFF_DATE_SYS_PROP_NAME));
}

SyntheticSourceLicenseService(Settings settings, String cutoffDate) {
this.syntheticSourceFallback = FALLBACK_SETTING.get(settings);
this.cutoffDate = getCutoffDate(cutoffDate);
}

/**
* @return whether synthetic source mode should fallback to stored source.
*/
public boolean fallbackToStoredSource(boolean isTemplateValidation) {
public boolean fallbackToStoredSource(boolean isTemplateValidation, boolean legacyLicensedUsageOfSyntheticSourceAllowed) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a little more time to look at this, and I wanna propose a refactor:

public boolean fallbackToStoredSource(boolean isTemplateValidation, boolean legacyLicensedUsageOfSyntheticSourceAllowed) {
    if (syntheticSourceFallback) {
        return true;
    }

    var licenseStateSnapshot = licenseState.copyCurrentLicenseState();
    if (checkFeature(SYNTHETIC_SOURCE_FEATURE, licenseStateSnapshot, isTemplateValidation)) {
        return false;
    }

    var license = licenseService.getLicense();
    if (license == null) {
        return true;
    }

    boolean beforeCutoffDate = license.startDate() <= cutoffDate;
    if (legacyLicensedUsageOfSyntheticSourceAllowed
        && beforeCutoffDate
        && checkFeature(SYNTHETIC_SOURCE_FEATURE_LEGACY, licenseStateSnapshot, isTemplateValidation)) {
        // platinum license will allow synthetic source with gold legacy licensed feature too.
        LOGGER.debug("legacy license [{}] is allowed to use synthetic source", licenseStateSnapshot.getOperationMode().description());
        return false;
    }

    return true;
}

private static boolean checkFeature(
    LicensedFeature.Momentary licensedFeature,
    XPackLicenseState licenseStateSnapshot,
    boolean isTemplateValidation
) {
    if (isTemplateValidation) {
        return licensedFeature.checkWithoutTracking(licenseStateSnapshot);
    } else {
        return licensedFeature.check(licenseStateSnapshot);
    }
}

This has two advantages:

  1. We short-circuit the happy path, i.e., for enterprise licenses, we skip all the extra logic around checking dates, getting the license from cluster state etc -- it feels more stream-lined this way
  2. We avoid the out-of-band operationMode equality check and just rely on the legacy feature check instead. This is more in line with how feature checks are meant to go (prefer using feature.check() over explicitly accessing operation mode).

LMKWYT!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense!

if (syntheticSourceFallback) {
return true;
}

var licenseStateSnapshot = licenseState.copyCurrentLicenseState();
if (checkFeature(SYNTHETIC_SOURCE_FEATURE, licenseStateSnapshot, isTemplateValidation)) {
return false;
}

var license = licenseService.getLicense();
if (license == null) {
return true;
}

boolean beforeCutoffDate = license.startDate() <= cutoffDate;
if (legacyLicensedUsageOfSyntheticSourceAllowed
&& beforeCutoffDate
&& checkFeature(SYNTHETIC_SOURCE_FEATURE_LEGACY, licenseStateSnapshot, isTemplateValidation)) {
// platinum license will allow synthetic source with gold legacy licensed feature too.
LOGGER.debug("legacy license [{}] is allowed to use synthetic source", licenseStateSnapshot.getOperationMode().description());
return false;
}

return true;
}

private static boolean checkFeature(
LicensedFeature.Momentary licensedFeature,
XPackLicenseState licenseStateSnapshot,
boolean isTemplateValidation
) {
if (isTemplateValidation) {
return SYNTHETIC_SOURCE_FEATURE.checkWithoutTracking(licenseState) == false;
return licensedFeature.checkWithoutTracking(licenseStateSnapshot);
} else {
return SYNTHETIC_SOURCE_FEATURE.check(licenseState) == false;
return licensedFeature.check(licenseStateSnapshot);
}
}

void setSyntheticSourceFallback(boolean syntheticSourceFallback) {
this.syntheticSourceFallback = syntheticSourceFallback;
}

void setLicenseService(LicenseService licenseService) {
this.licenseService = licenseService;
}

void setLicenseState(XPackLicenseState licenseState) {
this.licenseState = licenseState;
}

private static long getCutoffDate(String cutoffDateAsString) {
if (cutoffDateAsString != null) {
long cutoffDate = LocalDateTime.parse(cutoffDateAsString).toInstant(ZoneOffset.UTC).toEpochMilli();
LOGGER.warn("Configuring [{}] is only allowed with explicit approval from Elastic.", CUTOFF_DATE_SYS_PROP_NAME);
LOGGER.info(
"Configuring [{}] to [{}]",
CUTOFF_DATE_SYS_PROP_NAME,
LocalDateTime.ofInstant(Instant.ofEpochSecond(cutoffDate), ZoneOffset.UTC)
);
return cutoffDate;
} else {
return DEFAULT_CUTOFF_DATE;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* 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.xpack.logsdb;

import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.mapper.SourceFieldMapper;
import org.elasticsearch.license.AbstractLicensesIntegrationTestCase;
import org.elasticsearch.license.GetFeatureUsageRequest;
import org.elasticsearch.license.GetFeatureUsageResponse;
import org.elasticsearch.license.License;
import org.elasticsearch.license.LicenseService;
import org.elasticsearch.license.LicensedFeature;
import org.elasticsearch.license.TransportGetFeatureUsageAction;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
import org.hamcrest.Matcher;
import org.junit.Before;

import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Collection;
import java.util.List;

import static org.elasticsearch.test.ESIntegTestCase.Scope.TEST;
import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseServiceTests.createEnterpriseLicense;
import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseServiceTests.createGoldOrPlatinumLicense;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;

@ESIntegTestCase.ClusterScope(scope = TEST, numDataNodes = 1, numClientNodes = 0, supportsDedicatedMasters = false)
public class LegacyLicenceIntegrationTests extends AbstractLicensesIntegrationTestCase {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a few suggestions to beef up the coverage in this class (included as diffs below) but basically:

  1. We should check that feature usage is correctly recorded
  2. That enterprise licenses work, even with a start date after the cutoff, and for any index
  3. That valid gold or platinum licenses past cut-off fall back to stored source

We have these scenarios covered via unit tests but I think getting real integ coverage is important since there are quite a few moving parts where issues might get masked by mocking.

diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LegacyLicenceIntegrationTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LegacyLicenceIntegrationTests.java
index d20697369a2..5e7dc0da458 100644
--- a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LegacyLicenceIntegrationTests.java
+++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/LegacyLicenceIntegrationTests.java
@@ -11,21 +11,31 @@ import org.elasticsearch.action.admin.indices.settings.get.GetSettingsRequest;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.mapper.SourceFieldMapper;
 import org.elasticsearch.license.AbstractLicensesIntegrationTestCase;
+import org.elasticsearch.license.GetFeatureUsageRequest;
+import org.elasticsearch.license.GetFeatureUsageResponse;
 import org.elasticsearch.license.License;
 import org.elasticsearch.license.LicenseService;
+import org.elasticsearch.license.LicensedFeature;
+import org.elasticsearch.license.TransportGetFeatureUsageAction;
 import org.elasticsearch.license.XPackLicenseState;
 import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.test.ESIntegTestCase;
 import org.elasticsearch.xpack.core.LocalStateCompositeXPackPlugin;
+import org.hamcrest.Matcher;
 import org.junit.Before;
 
 import java.nio.file.Path;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
 import java.util.Collection;
 import java.util.List;
 
 import static org.elasticsearch.test.ESIntegTestCase.Scope.TEST;
+import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseServiceTests.createEnterpriseLicense;
 import static org.elasticsearch.xpack.logsdb.SyntheticSourceLicenseServiceTests.createGoldOrPlatinumLicense;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 
 @ESIntegTestCase.ClusterScope(scope = TEST, numDataNodes = 1, numClientNodes = 0, supportsDedicatedMasters = false)
 public class LegacyLicenceIntegrationTests extends AbstractLicensesIntegrationTestCase {
@@ -44,28 +54,77 @@ public class LegacyLicenceIntegrationTests extends AbstractLicensesIntegrationTe
         ensureGreen();
     }
 
-    public void testSyntheticSourceUsageDisallowed() throws Exception {
-        String indexName = "test";
-        var settings = Settings.builder().put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), "synthetic").build();
-        createIndex(indexName, settings);
-        var response = admin().indices().getSettings(new GetSettingsRequest().indices(indexName)).actionGet();
-        assertThat(
-            response.getIndexToSettings().get(indexName).get(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey()),
-            equalTo("STORED")
-        );
+    public void testSyntheticSourceUsageDisallowed() {
+        createIndexAndAssertExpectedType("test", "STORED");
+
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, nullValue());
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, nullValue());
+    }
+
+    public void testSyntheticSourceUsageWithLegacyLicense() {
+        createIndexAndAssertExpectedType(".profiling-stacktraces", "synthetic");
+
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, not(nullValue()));
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, nullValue());
+    }
+
+    public void testSyntheticSourceUsageWithLegacyLicensePastCutoff() throws Exception {
+        long startPastCutoff = LocalDateTime.of(2025, 11, 12, 0, 0).toInstant(ZoneOffset.UTC).toEpochMilli();
+        putLicense(createGoldOrPlatinumLicense(startPastCutoff));
+        ensureGreen();
+
+        createIndexAndAssertExpectedType(".profiling-stacktraces", "STORED");
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, nullValue());
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, nullValue());
     }
 
-    public void testSyntheticSourceUsageWithLegacyLicense() throws Exception {
-        String indexName = ".profiling-stacktraces";
+    public void testSyntheticSourceUsageWithEnterpriseLicensePastCutoff() throws Exception {
+        long startPastCutoff = LocalDateTime.of(2025, 11, 12, 0, 0).toInstant(ZoneOffset.UTC).toEpochMilli();
+        putLicense(createEnterpriseLicense(startPastCutoff));
+        ensureGreen();
+
+        createIndexAndAssertExpectedType(".profiling-traces", "synthetic");
+        // also supports non-exceptional indices
+        createIndexAndAssertExpectedType("test", "synthetic");
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, nullValue());
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, not(nullValue()));
+    }
+
+    public void testSyntheticSourceUsageTracksBothLegacyAndRegularFeature() throws Exception {
+        createIndexAndAssertExpectedType(".profiling-traces", "synthetic");
+
+        putLicense(createEnterpriseLicense());
+        ensureGreen();
+
+        createIndexAndAssertExpectedType(".profiling-traces-v2", "synthetic");
+
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, not(nullValue()));
+        assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, not(nullValue()));
+    }
+
+    private void createIndexAndAssertExpectedType(String indexName, String expectedType) {
         var settings = Settings.builder().put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), "synthetic").build();
         createIndex(indexName, settings);
         var response = admin().indices().getSettings(new GetSettingsRequest().indices(indexName)).actionGet();
         assertThat(
             response.getIndexToSettings().get(indexName).get(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey()),
-            equalTo("synthetic")
+            equalTo(expectedType)
         );
     }
 
+    private List<GetFeatureUsageResponse.FeatureUsageInfo> getFeatureUsageInfo() {
+        return client().execute(TransportGetFeatureUsageAction.TYPE, new GetFeatureUsageRequest()).actionGet().getFeatures();
+    }
+
+    private void assertFeatureUsage(LicensedFeature.Momentary syntheticSourceFeature, Matcher<Object> matcher) {
+        GetFeatureUsageResponse.FeatureUsageInfo featureUsage = getFeatureUsageInfo().stream()
+            .filter(f -> f.getFamily().equals(SyntheticSourceLicenseService.MAPPINGS_FEATURE_FAMILY))
+            .filter(f -> f.getName().equals(syntheticSourceFeature.getName()))
+            .findAny()
+            .orElse(null);
+        assertThat(featureUsage, matcher);
+    }
+
     public static class P extends LocalStateCompositeXPackPlugin {
 
         public P(final Settings settings, final Path configPath) {

And:

diff --git a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseServiceTests.java b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseServiceTests.java
index e9f530ff868..682749bd436 100644
--- a/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseServiceTests.java
+++ b/x-pack/plugin/logsdb/src/test/java/org/elasticsearch/xpack/logsdb/SyntheticSourceLicenseServiceTests.java
@@ -34,7 +34,7 @@ public class SyntheticSourceLicenseServiceTests extends ESTestCase {
     @Before
     public void setup() throws Exception {
         mockLicenseService = mock(LicenseService.class);
-        License license = createEnterpirseLicense();
+        License license = createEnterpriseLicense();
         when(mockLicenseService.getLicense()).thenReturn(license);
         licenseService = new SyntheticSourceLicenseService(Settings.EMPTY);
     }
@@ -163,15 +163,19 @@ public class SyntheticSourceLicenseServiceTests extends ESTestCase {
         assertEquals("Provided cutoff date is beyond max cutoff date", e.getMessage());
     }
 
-    static License createEnterpirseLicense() throws Exception {
+    static License createEnterpriseLicense() throws Exception {
         long start = LocalDateTime.of(2024, 11, 12, 0, 0).toInstant(ZoneOffset.UTC).toEpochMilli();
+        return createEnterpriseLicense(start);
+    }
+
+    static License createEnterpriseLicense(long start) throws Exception {
         String uid = UUID.randomUUID().toString();
         final License.Builder builder = License.builder()
             .uid(uid)
             .version(License.VERSION_CURRENT)
             .expiryDate(dateMath("now+2d", System.currentTimeMillis()))
             .startDate(start)
-            .issueDate(start)
+            .issueDate(System.currentTimeMillis())
             .type("enterprise")
             .issuedTo("customer")
             .issuer("elasticsearch")
@@ -191,7 +195,7 @@ public class SyntheticSourceLicenseServiceTests extends ESTestCase {
             .version(License.VERSION_CURRENT)
             .expiryDate(dateMath("now+100d", System.currentTimeMillis()))
             .startDate(start)
-            .issueDate(start)
+            .issueDate(System.currentTimeMillis())
             .type(randomBoolean() ? "gold" : "platinum")
             .issuedTo("customer")
             .issuer("elasticsearch")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and thanks for beefing up the test coverage here.


@Override
protected Collection<Class<? extends Plugin>> nodePlugins() {
return List.of(P.class);
}

@Before
public void setup() throws Exception {
wipeAllLicenses();
ensureGreen();
License license = createGoldOrPlatinumLicense();
putLicense(license);
ensureGreen();
}

public void testSyntheticSourceUsageDisallowed() {
createIndexWithSyntheticSourceAndAssertExpectedType("test", "STORED");

assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, nullValue());
assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, nullValue());
}

public void testSyntheticSourceUsageWithLegacyLicense() {
createIndexWithSyntheticSourceAndAssertExpectedType(".profiling-stacktraces", "synthetic");

assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, not(nullValue()));
assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, nullValue());
}

public void testSyntheticSourceUsageWithLegacyLicensePastCutoff() throws Exception {
long startPastCutoff = LocalDateTime.of(2025, 11, 12, 0, 0).toInstant(ZoneOffset.UTC).toEpochMilli();
putLicense(createGoldOrPlatinumLicense(startPastCutoff));
ensureGreen();

createIndexWithSyntheticSourceAndAssertExpectedType(".profiling-stacktraces", "STORED");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should test with synthetic?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this method creates an index with source mode set to synthetic. The last argument here is what the expected source mode should be.

assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, nullValue());
assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, nullValue());
}

public void testSyntheticSourceUsageWithEnterpriseLicensePastCutoff() throws Exception {
long startPastCutoff = LocalDateTime.of(2025, 11, 12, 0, 0).toInstant(ZoneOffset.UTC).toEpochMilli();
putLicense(createEnterpriseLicense(startPastCutoff));
ensureGreen();

createIndexWithSyntheticSourceAndAssertExpectedType(".profiling-traces", "synthetic");
// also supports non-exceptional indices
createIndexWithSyntheticSourceAndAssertExpectedType("test", "synthetic");
assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, nullValue());
assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, not(nullValue()));
}

public void testSyntheticSourceUsageTracksBothLegacyAndRegularFeature() throws Exception {
createIndexWithSyntheticSourceAndAssertExpectedType(".profiling-traces", "synthetic");

putLicense(createEnterpriseLicense());
ensureGreen();

createIndexWithSyntheticSourceAndAssertExpectedType(".profiling-traces-v2", "synthetic");

assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE_LEGACY, not(nullValue()));
assertFeatureUsage(SyntheticSourceLicenseService.SYNTHETIC_SOURCE_FEATURE, not(nullValue()));
}

private void createIndexWithSyntheticSourceAndAssertExpectedType(String indexName, String expectedType) {
var settings = Settings.builder().put(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey(), "synthetic").build();
createIndex(indexName, settings);
var response = admin().indices().getSettings(new GetSettingsRequest().indices(indexName)).actionGet();
assertThat(
response.getIndexToSettings().get(indexName).get(SourceFieldMapper.INDEX_MAPPER_SOURCE_MODE_SETTING.getKey()),
equalTo(expectedType)
);
}

private List<GetFeatureUsageResponse.FeatureUsageInfo> getFeatureUsageInfo() {
return client().execute(TransportGetFeatureUsageAction.TYPE, new GetFeatureUsageRequest()).actionGet().getFeatures();
}

private void assertFeatureUsage(LicensedFeature.Momentary syntheticSourceFeature, Matcher<Object> matcher) {
GetFeatureUsageResponse.FeatureUsageInfo featureUsage = getFeatureUsageInfo().stream()
.filter(f -> f.getFamily().equals(SyntheticSourceLicenseService.MAPPINGS_FEATURE_FAMILY))
.filter(f -> f.getName().equals(syntheticSourceFeature.getName()))
.findAny()
.orElse(null);
assertThat(featureUsage, matcher);
}

public static class P extends LocalStateCompositeXPackPlugin {

public P(final Settings settings, final Path configPath) {
super(settings, configPath);
plugins.add(new LogsDBPlugin(settings) {
@Override
protected XPackLicenseState getLicenseState() {
return P.this.getLicenseState();
}

@Override
protected LicenseService getLicenseService() {
return P.this.getLicenseService();
}
});
}

}
}
Loading