-
Notifications
You must be signed in to change notification settings - Fork 25.6k
[Downsampling++] Add time series telemetry in xpack usage #134214
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
Merged
elasticsearchmachine
merged 11 commits into
elastic:main
from
gmarouli:downsampling++/add-basic-telemetry
Sep 8, 2025
+1,129
−1
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
3d81107
Add time series telemetry in xpack usage
gmarouli 5e46643
Update docs/changelog/134214.yaml
gmarouli bd34cb5
Merge branch 'main' into downsampling++/add-basic-telemetry
gmarouli 5ebef0e
Merge branch 'main' into downsampling++/add-basic-telemetry
gmarouli df8a474
Grant new xpack field non-operator access
gmarouli 543fe12
Merge branch 'main' into downsampling++/add-basic-telemetry
gmarouli 69f7642
Polish
gmarouli 1413cc7
Apply review feedback
gmarouli ae4412f
Add null check for dlm stats
gmarouli 7aa255f
Add comment about omitting ILM stats
gmarouli 9b68b0c
Merge branch 'main' into downsampling++/add-basic-telemetry
gmarouli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| pr: 134214 | ||
| summary: "[Downsampling++] Add time series telemetry in xpack usage" | ||
| area: Downsampling | ||
| type: enhancement | ||
| issues: | ||
| - 133953 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
533 changes: 533 additions & 0 deletions
533
...lusterTest/java/org/elasticsearch/xpack/core/action/TimeSeriesUsageTransportActionIT.java
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
193 changes: 193 additions & 0 deletions
193
...ore/src/main/java/org/elasticsearch/xpack/core/action/TimeSeriesUsageTransportAction.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| /* | ||
| * 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.core.action; | ||
|
|
||
| import org.elasticsearch.action.ActionListener; | ||
| import org.elasticsearch.action.support.ActionFilters; | ||
| import org.elasticsearch.cluster.ClusterState; | ||
| import org.elasticsearch.cluster.metadata.DataStream; | ||
| import org.elasticsearch.cluster.metadata.DataStreamLifecycle; | ||
| import org.elasticsearch.cluster.metadata.IndexMetadata; | ||
| import org.elasticsearch.cluster.metadata.ProjectMetadata; | ||
| import org.elasticsearch.cluster.project.ProjectResolver; | ||
| import org.elasticsearch.cluster.service.ClusterService; | ||
| import org.elasticsearch.index.Index; | ||
| import org.elasticsearch.index.IndexMode; | ||
| import org.elasticsearch.injection.guice.Inject; | ||
| import org.elasticsearch.protocol.xpack.XPackUsageRequest; | ||
| import org.elasticsearch.tasks.Task; | ||
| import org.elasticsearch.threadpool.ThreadPool; | ||
| import org.elasticsearch.transport.TransportService; | ||
| import org.elasticsearch.xpack.core.datastreams.TimeSeriesFeatureSetUsage; | ||
| import org.elasticsearch.xpack.core.ilm.DownsampleAction; | ||
| import org.elasticsearch.xpack.core.ilm.IndexLifecycleMetadata; | ||
| import org.elasticsearch.xpack.core.ilm.LifecyclePolicy; | ||
| import org.elasticsearch.xpack.core.ilm.LifecyclePolicyMetadata; | ||
| import org.elasticsearch.xpack.core.ilm.Phase; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.LongSummaryStatistics; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * Exposes the time series telemetry via the xpack usage API. We track the following only for time series data streams: | ||
| * - time series data stream count | ||
| * - time series backing indices of these time series data streams | ||
| * - the feature that downsamples the time series data streams, we use the write index to avoid resolving templates, | ||
| * this might cause a small delay in the counters (backing indices, downsampling rounds). | ||
| * - For ILM specifically, we count the phases that have configured downsampling in the policies used in the time series data streams. | ||
| * - When elasticsearch is running in DLM only mode, we skip all the ILM metrics. | ||
| */ | ||
| public class TimeSeriesUsageTransportAction extends XPackUsageFeatureTransportAction { | ||
|
|
||
| private final ProjectResolver projectResolver; | ||
| private final boolean ilmAvailable; | ||
|
|
||
| @Inject | ||
| public TimeSeriesUsageTransportAction( | ||
| TransportService transportService, | ||
| ClusterService clusterService, | ||
| ThreadPool threadPool, | ||
| ActionFilters actionFilters, | ||
| ProjectResolver projectResolver | ||
| ) { | ||
| super(XPackUsageFeatureAction.TIME_SERIES_DATA_STREAMS.name(), transportService, clusterService, threadPool, actionFilters); | ||
| this.projectResolver = projectResolver; | ||
| this.ilmAvailable = DataStreamLifecycle.isDataStreamsLifecycleOnlyMode(clusterService.getSettings()) == false; | ||
| } | ||
|
|
||
| @Override | ||
| protected void localClusterStateOperation( | ||
| Task task, | ||
| XPackUsageRequest request, | ||
| ClusterState state, | ||
| ActionListener<XPackUsageFeatureResponse> listener | ||
| ) { | ||
| ProjectMetadata projectMetadata = projectResolver.getProjectMetadata(state); | ||
| IndexLifecycleMetadata ilmMetadata = projectMetadata.custom(IndexLifecycleMetadata.TYPE, IndexLifecycleMetadata.EMPTY); | ||
| final Map<String, DataStream> dataStreams = projectMetadata.dataStreams(); | ||
|
|
||
| long tsDataStreamCount = 0; | ||
| long tsIndexCount = 0; | ||
| IlmDownsamplingStatsTracker ilmStats = ilmAvailable ? new IlmDownsamplingStatsTracker() : null; | ||
| DownsamplingStatsTracker dlmStats = new DownsamplingStatsTracker(); | ||
| Map<String, Long> indicesByInterval = new HashMap<>(); | ||
|
|
||
| for (DataStream ds : dataStreams.values()) { | ||
| // We choose to not count time series backing indices that do not belong to a time series data stream. | ||
| if (ds.getIndexMode() != IndexMode.TIME_SERIES) { | ||
| continue; | ||
| } | ||
| tsDataStreamCount++; | ||
| Integer dlmRounds = ds.getDataLifecycle() == null || ds.getDataLifecycle().downsampling() == null | ||
| ? null | ||
| : ds.getDataLifecycle().downsampling().size(); | ||
|
|
||
| for (Index backingIndex : ds.getIndices()) { | ||
| IndexMetadata indexMetadata = projectMetadata.index(backingIndex); | ||
| if (indexMetadata.getIndexMode() != IndexMode.TIME_SERIES) { | ||
| continue; | ||
| } | ||
| tsIndexCount++; | ||
| if (ds.isIndexManagedByDataStreamLifecycle(indexMetadata.getIndex(), ignored -> indexMetadata) && dlmRounds != null) { | ||
| dlmStats.trackIndex(ds, indexMetadata); | ||
| dlmStats.trackRounds(dlmRounds, ds, indexMetadata); | ||
| } else if (ilmAvailable && projectMetadata.isIndexManagedByILM(indexMetadata)) { | ||
| LifecyclePolicyMetadata policyMetadata = ilmMetadata.getPolicyMetadatas().get(indexMetadata.getLifecyclePolicyName()); | ||
| if (policyMetadata == null) { | ||
| continue; | ||
| } | ||
| int rounds = 0; | ||
| for (Phase phase : policyMetadata.getPolicy().getPhases().values()) { | ||
| if (phase.getActions().containsKey(DownsampleAction.NAME)) { | ||
| rounds++; | ||
| } | ||
| } | ||
| if (rounds > 0) { | ||
| ilmStats.trackPolicy(policyMetadata.getPolicy()); | ||
| ilmStats.trackIndex(ds, indexMetadata); | ||
| ilmStats.trackRounds(rounds, ds, indexMetadata); | ||
| } | ||
| } | ||
| String interval = indexMetadata.getSettings().get(IndexMetadata.INDEX_DOWNSAMPLE_INTERVAL.getKey()); | ||
| if (interval != null) { | ||
| Long count = indicesByInterval.computeIfAbsent(interval, ignored -> 0L); | ||
| indicesByInterval.put(interval, count + 1); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| final TimeSeriesFeatureSetUsage usage = ilmAvailable | ||
| ? new TimeSeriesFeatureSetUsage( | ||
| tsDataStreamCount, | ||
| tsIndexCount, | ||
| ilmStats.getDownsamplingStats(), | ||
| ilmStats.getIlmPolicyStats(), | ||
| dlmStats.getDownsamplingStats(), | ||
| indicesByInterval | ||
| ) | ||
| : new TimeSeriesFeatureSetUsage(tsDataStreamCount, tsIndexCount, dlmStats.getDownsamplingStats(), indicesByInterval); | ||
| listener.onResponse(new XPackUsageFeatureResponse(usage)); | ||
| } | ||
|
|
||
| private static class DownsamplingStatsTracker { | ||
| private long downsampledDataStreams = 0; | ||
| private long downsampledIndices = 0; | ||
| private final LongSummaryStatistics rounds = new LongSummaryStatistics(); | ||
|
|
||
| void trackIndex(DataStream ds, IndexMetadata indexMetadata) { | ||
| if (Objects.equals(indexMetadata.getIndex(), ds.getWriteIndex())) { | ||
| downsampledDataStreams++; | ||
| } | ||
| downsampledIndices++; | ||
| } | ||
|
|
||
| void trackRounds(int rounds, DataStream ds, IndexMetadata indexMetadata) { | ||
| // We want to track rounds per data stream, so we use the write index to determine the | ||
| // rounds applicable for this data stream | ||
| if (Objects.equals(indexMetadata.getIndex(), ds.getWriteIndex())) { | ||
| this.rounds.accept(rounds); | ||
| } | ||
| } | ||
|
|
||
| TimeSeriesFeatureSetUsage.DownsamplingFeatureStats getDownsamplingStats() { | ||
| return new TimeSeriesFeatureSetUsage.DownsamplingFeatureStats( | ||
| downsampledDataStreams, | ||
| downsampledIndices, | ||
| rounds.getMin(), | ||
| rounds.getAverage(), | ||
| rounds.getMax() | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| static class IlmDownsamplingStatsTracker extends DownsamplingStatsTracker { | ||
| private final Map<String, Map<String, Phase>> policies = new HashMap<>(); | ||
|
|
||
| void trackPolicy(LifecyclePolicy ilmPolicy) { | ||
| policies.putIfAbsent(ilmPolicy.getName(), ilmPolicy.getPhases()); | ||
| } | ||
|
|
||
| Map<String, Long> getIlmPolicyStats() { | ||
| if (policies.isEmpty()) { | ||
| return Map.of(); | ||
| } | ||
| Map<String, Long> downsamplingPhases = new HashMap<>(); | ||
| for (String ilmPolicy : policies.keySet()) { | ||
| for (Phase phase : policies.get(ilmPolicy).values()) { | ||
| if (phase.getActions().containsKey(DownsampleAction.NAME)) { | ||
| Long current = downsamplingPhases.computeIfAbsent(phase.getName(), ignored -> 0L); | ||
| downsamplingPhases.put(phase.getName(), current + 1); | ||
| } | ||
| } | ||
| } | ||
| return downsamplingPhases; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why
dlmStats?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean why I picked this variable name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought this call is for ilmStats, surprised to see dlmStats for this arg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. There are two different constructors one that accepts stats for both ILM and DLM and one that only accepts DLM for the serverless use case, this was syntactic sugar to make more explicit that ILM stats would be null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add a comment for clarity