Skip to content

Commit 7a1d2ea

Browse files
gmaroulicbuescher
authored andcommitted
Expose global retention settings via data stream lifecycle API (elastic#112210)
In this PR we expose the global retention via the `GET _data_stream/{target}/_lifecycle` API. Since the global retention is a main feature of the data stream lifecycle we chose to expose it by default. ``` GET /_data_stream/my-data-stream/_lifecycle { "global_retention": { "default_retention": "7d", "max_retention": "365d" }, "data_streams": [...] } ```
1 parent 2c4d68c commit 7a1d2ea

File tree

9 files changed

+206
-27
lines changed

9 files changed

+206
-27
lines changed

docs/changelog/112210.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 112210
2+
summary: Expose global retention settings via data stream lifecycle API
3+
area: Data streams
4+
type: enhancement
5+
issues: []

docs/reference/data-streams/lifecycle/apis/get-lifecycle.asciidoc

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,18 @@ Name of the data stream.
6767
=====
6868
`data_retention`::
6969
(Optional, string)
70+
If defined, it represents the retention requested by the data stream owner for this data stream.
71+
72+
`effective_retention`::
73+
(Optional, string)
7074
If defined, every document added to this data stream will be stored at least for this time frame. Any time after this
71-
duration the document could be deleted. When undefined, every document in this data stream will be stored indefinitely.
75+
duration the document could be deleted. When empty, every document in this data stream will be stored indefinitely.
76+
duration the document could be deleted. When empty, every document in this data stream will be stored indefinitely. The
77+
effective retention is calculated as described in the <<effective-retention-calculation, tutorial>>.
78+
79+
`retention_determined_by`::
80+
(Optional, string)
81+
The source of the retention, it can be one of three values, `data_stream_configuration`, `default_retention` or `max_retention`.
7282

7383
`rollover`::
7484
(Optional, object)
@@ -78,6 +88,21 @@ when the query param `include_defaults` is set to `true`. The contents of this f
7888
=====
7989
====
8090

91+
`global_retention`::
92+
(object)
93+
Contains the global max and default retention. When no global retention is configured, this will be an empty object.
94+
+
95+
.Properties of `global_retention`
96+
[%collapsible%open]
97+
====
98+
`max_retention`::
99+
(Optional, string)
100+
The effective retention of data streams managed by the data stream lifecycle cannot exceed this value.
101+
`default_retention`::
102+
(Optional, string)
103+
This will be the effective retention of data streams managed by the data stream lifecycle that do not specify `data_retention`.
104+
====
105+
81106
[[data-streams-get-lifecycle-example]]
82107
==== {api-examples-title}
83108

@@ -142,6 +167,7 @@ The response will look like the following:
142167
"retention_determined_by": "data_stream_configuration"
143168
}
144169
}
145-
]
170+
],
171+
"global_retention": {}
146172
}
147173
--------------------------------------------------

docs/reference/data-streams/lifecycle/tutorial-manage-data-stream-retention.asciidoc

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,29 @@ We see that it will remain the same with what the user configured:
189189
[source,console-result]
190190
----
191191
{
192+
"global_retention" : {
193+
"max_retention" : "90d", <1>
194+
"default_retention" : "7d" <2>
195+
},
192196
"data_streams": [
193197
{
194198
"name": "my-data-stream",
195199
"lifecycle": {
196200
"enabled": true,
197-
"data_retention": "30d",
198-
"effective_retention": "30d",
199-
"retention_determined_by": "data_stream_configuration"
201+
"data_retention": "30d", <3>
202+
"effective_retention": "30d", <4>
203+
"retention_determined_by": "data_stream_configuration" <5>
200204
}
201205
}
202206
]
203207
}
204208
----
209+
<1> The maximum retention configured in the cluster.
210+
<2> The default retention configured in the cluster.
211+
<3> The requested retention for this data stream.
212+
<4> The retention that is applied by the data stream lifecycle on this data stream.
213+
<5> The configuration that determined the effective retention. In this case it's the `data_configuration` because
214+
it is less than the `max_retention`.
205215

206216
[discrete]
207217
[[effective-retention-application]]

docs/reference/data-streams/lifecycle/tutorial-manage-new-data-stream.asciidoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ The result will look like this:
9999
"retention_determined_by": "data_stream_configuration"
100100
}
101101
}
102-
]
102+
],
103+
"global_retention": {}
103104
}
104105
--------------------------------------------------
105106
<1> The name of your data stream.

modules/data-streams/src/main/java/org/elasticsearch/datastreams/lifecycle/rest/RestGetDataStreamLifecycleAction.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ public boolean allowSystemIndexAccessByDefault() {
5959

6060
@Override
6161
public Set<String> supportedCapabilities() {
62-
return Set.of(DataStreamLifecycle.EFFECTIVE_RETENTION_REST_API_CAPABILITY);
62+
return Set.of(DataStreamLifecycle.EFFECTIVE_RETENTION_REST_API_CAPABILITY, "data_stream_global_retention");
6363
}
6464
}

modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/lifecycle/20_basic.yml

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
setup:
2-
- skip:
3-
features: allowed_warnings
4-
cluster_features: ["gte_v8.11.0"]
5-
reason: "Data stream lifecycles only supported in 8.11+"
2+
- requires:
3+
cluster_features: [ "gte_v8.11.0" ]
4+
reason: "Data stream lifecycle was released as tech preview in 8.11"
5+
test_runner_features: allowed_warnings
66
- do:
77
allowed_warnings:
88
- "index template [my-lifecycle] has index patterns [data-stream-with-lifecycle] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-lifecycle] will take precedence during new index creation"
@@ -25,6 +25,7 @@ setup:
2525
body:
2626
index_patterns: [simple-data-stream1]
2727
template:
28+
lifecycle: {}
2829
mappings:
2930
properties:
3031
'@timestamp':
@@ -39,27 +40,93 @@ setup:
3940
name: simple-data-stream1
4041

4142
---
42-
"Get data stream lifecycle":
43+
teardown:
44+
- requires:
45+
reason: "Global retention was exposed in 8.16+"
46+
test_runner_features: [ capabilities ]
47+
capabilities:
48+
- method: GET
49+
path: /_data_stream/{index}/_lifecycle
50+
capabilities: [ 'data_stream_global_retention' ]
51+
- do:
52+
cluster.put_settings:
53+
body:
54+
persistent:
55+
data_streams.lifecycle.retention.max: null
56+
data_streams.lifecycle.retention.default: null
4357

58+
---
59+
"Get data stream lifecycle":
60+
- requires:
61+
reason: "Global retention was exposed in 8.16+"
62+
test_runner_features: [ capabilities ]
63+
capabilities:
64+
- method: GET
65+
path: /_data_stream/{index}/_lifecycle
66+
capabilities: [ 'data_stream_global_retention' ]
4467
- do:
4568
indices.get_data_lifecycle:
4669
name: "data-stream-with-lifecycle"
4770
- length: { data_streams: 1}
4871
- match: { data_streams.0.name: data-stream-with-lifecycle }
4972
- match: { data_streams.0.lifecycle.data_retention: '10d' }
73+
- match: { data_streams.0.lifecycle.effective_retention: '10d' }
5074
- match: { data_streams.0.lifecycle.enabled: true}
75+
- match: { global_retention: {} }
5176

5277
---
53-
"Get data stream with default lifecycle":
54-
- skip:
55-
awaits_fix: https://github.com/elastic/elasticsearch/pull/100187
78+
"Get data stream with default lifecycle configuration":
79+
- requires:
80+
reason: "Global retention was exposed in 8.16+"
81+
test_runner_features: [ capabilities ]
82+
capabilities:
83+
- method: GET
84+
path: /_data_stream/{index}/_lifecycle
85+
capabilities: [ 'data_stream_global_retention' ]
86+
- do:
87+
indices.get_data_lifecycle:
88+
name: "simple-data-stream1"
89+
- length: { data_streams: 1}
90+
- match: { data_streams.0.name: simple-data-stream1 }
91+
- match: { data_streams.0.lifecycle.enabled: true}
92+
- is_false: data_streams.0.lifecycle.effective_retention
93+
- match: { global_retention: {} }
5694

95+
---
96+
"Get data stream with global retention":
97+
- requires:
98+
reason: "Global retention was exposed in 8.16+"
99+
test_runner_features: [ capabilities ]
100+
capabilities:
101+
- method: GET
102+
path: /_data_stream/{index}/_lifecycle
103+
capabilities: [ 'data_stream_global_retention' ]
104+
- do:
105+
cluster.put_settings:
106+
body:
107+
persistent:
108+
data_streams.lifecycle.retention.default: "7d"
109+
data_streams.lifecycle.retention.max: "9d"
57110
- do:
58111
indices.get_data_lifecycle:
59112
name: "simple-data-stream1"
60113
- length: { data_streams: 1}
61114
- match: { data_streams.0.name: simple-data-stream1 }
62115
- match: { data_streams.0.lifecycle.enabled: true}
116+
- match: { data_streams.0.lifecycle.effective_retention: '7d'}
117+
- match: { global_retention.default_retention: '7d' }
118+
- match: { global_retention.max_retention: '9d' }
119+
120+
- do:
121+
indices.get_data_lifecycle:
122+
name: "data-stream-with-lifecycle"
123+
- length: { data_streams: 1 }
124+
- match: { data_streams.0.name: data-stream-with-lifecycle }
125+
- match: { data_streams.0.lifecycle.data_retention: '10d' }
126+
- match: { data_streams.0.lifecycle.effective_retention: '9d' }
127+
- match: { data_streams.0.lifecycle.enabled: true }
128+
- match: { global_retention.default_retention: '7d' }
129+
- match: { global_retention.max_retention: '9d' }
63130

64131
---
65132
"Put data stream lifecycle":

modules/data-streams/src/yamlRestTest/resources/rest-api-spec/test/data_stream/lifecycle/30_not_found.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
setup:
2-
- skip:
3-
features: allowed_warnings
4-
cluster_features: ["gte_v8.11.0"]
5-
reason: "Data stream lifecycle was GA in 8.11"
2+
- requires:
3+
cluster_features: [ "gte_v8.11.0" ]
4+
reason: "Data stream lifecycle was released as tech preview in 8.11"
5+
test_runner_features: allowed_warnings
66
- do:
77
allowed_warnings:
88
- "index template [my-lifecycle] has index patterns [my-data-stream-1] matching patterns from existing older templates [global] with patterns (global => [*]); this template [my-lifecycle] will take precedence during new index creation"

server/src/main/java/org/elasticsearch/action/datastreams/lifecycle/GetDataStreamLifecycleAction.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,16 @@ public void writeTo(StreamOutput out) throws IOException {
254254
public Iterator<ToXContent> toXContentChunked(ToXContent.Params outerParams) {
255255
return Iterators.concat(Iterators.single((builder, params) -> {
256256
builder.startObject();
257+
builder.startObject("global_retention");
258+
if (globalRetention != null) {
259+
if (globalRetention.maxRetention() != null) {
260+
builder.field("max_retention", globalRetention.maxRetention().getStringRep());
261+
}
262+
if (globalRetention.defaultRetention() != null) {
263+
builder.field("default_retention", globalRetention.defaultRetention().getStringRep());
264+
}
265+
}
266+
builder.endObject();
257267
builder.startArray(DATA_STREAMS_FIELD.getPreferredName());
258268
return builder;
259269
}),

server/src/test/java/org/elasticsearch/action/datastreams/lifecycle/GetDataStreamLifecycleActionTests.java

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.cluster.metadata.DataStreamGlobalRetention;
1313
import org.elasticsearch.cluster.metadata.DataStreamLifecycle;
1414
import org.elasticsearch.common.Strings;
15+
import org.elasticsearch.common.bytes.BytesReference;
1516
import org.elasticsearch.common.xcontent.XContentHelper;
1617
import org.elasticsearch.core.TimeValue;
1718
import org.elasticsearch.test.ESTestCase;
@@ -20,26 +21,88 @@
2021
import org.elasticsearch.xcontent.XContentType;
2122

2223
import java.io.IOException;
24+
import java.util.List;
2325
import java.util.Map;
2426

25-
import static org.elasticsearch.rest.RestRequest.PATH_RESTRICTED;
27+
import static org.elasticsearch.xcontent.ToXContent.EMPTY_PARAMS;
2628
import static org.hamcrest.Matchers.equalTo;
29+
import static org.hamcrest.Matchers.nullValue;
2730

2831
public class GetDataStreamLifecycleActionTests extends ESTestCase {
2932

33+
@SuppressWarnings("unchecked")
34+
public void testDefaultLifecycleResponseToXContent() throws Exception {
35+
boolean isInternalDataStream = randomBoolean();
36+
GetDataStreamLifecycleAction.Response.DataStreamLifecycle dataStreamLifecycle = createDataStreamLifecycle(
37+
DataStreamLifecycle.DEFAULT,
38+
isInternalDataStream
39+
);
40+
GetDataStreamLifecycleAction.Response response = new GetDataStreamLifecycleAction.Response(List.of(dataStreamLifecycle));
41+
try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
42+
builder.humanReadable(true);
43+
response.toXContentChunked(ToXContent.EMPTY_PARAMS).forEachRemaining(xcontent -> {
44+
try {
45+
xcontent.toXContent(builder, EMPTY_PARAMS);
46+
} catch (IOException e) {
47+
logger.error(e.getMessage(), e);
48+
fail(e.getMessage());
49+
}
50+
});
51+
Map<String, Object> resultMap = XContentHelper.convertToMap(BytesReference.bytes(builder), false, builder.contentType()).v2();
52+
assertThat(resultMap.get("global_retention"), equalTo(Map.of()));
53+
assertThat(resultMap.containsKey("data_streams"), equalTo(true));
54+
List<Map<String, Object>> dataStreams = (List<Map<String, Object>>) resultMap.get("data_streams");
55+
Map<String, Object> firstDataStream = dataStreams.get(0);
56+
assertThat(firstDataStream.containsKey("lifecycle"), equalTo(true));
57+
Map<String, Object> lifecycleResult = (Map<String, Object>) firstDataStream.get("lifecycle");
58+
assertThat(lifecycleResult.get("enabled"), equalTo(true));
59+
assertThat(lifecycleResult.get("data_retention"), nullValue());
60+
assertThat(lifecycleResult.get("effective_retention"), nullValue());
61+
assertThat(lifecycleResult.get("retention_determined_by"), nullValue());
62+
}
63+
}
64+
65+
@SuppressWarnings("unchecked")
66+
public void testGlobalRetentionToXContent() {
67+
TimeValue globalDefaultRetention = TimeValue.timeValueDays(10);
68+
TimeValue globalMaxRetention = TimeValue.timeValueDays(50);
69+
DataStreamGlobalRetention globalRetention = new DataStreamGlobalRetention(globalDefaultRetention, globalMaxRetention);
70+
GetDataStreamLifecycleAction.Response response = new GetDataStreamLifecycleAction.Response(List.of(), null, globalRetention);
71+
try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
72+
builder.humanReadable(true);
73+
response.toXContentChunked(ToXContent.EMPTY_PARAMS).forEachRemaining(xcontent -> {
74+
try {
75+
xcontent.toXContent(builder, EMPTY_PARAMS);
76+
} catch (IOException e) {
77+
logger.error(e.getMessage(), e);
78+
fail(e.getMessage());
79+
}
80+
});
81+
Map<String, Object> resultMap = XContentHelper.convertToMap(BytesReference.bytes(builder), false, builder.contentType()).v2();
82+
assertThat(resultMap.containsKey("global_retention"), equalTo(true));
83+
Map<String, String> globalRetentionMap = (Map<String, String>) resultMap.get("global_retention");
84+
assertThat(globalRetentionMap.get("max_retention"), equalTo(globalMaxRetention.getStringRep()));
85+
assertThat(globalRetentionMap.get("default_retention"), equalTo(globalDefaultRetention.getStringRep()));
86+
assertThat(resultMap.containsKey("data_streams"), equalTo(true));
87+
} catch (Exception e) {
88+
fail(e);
89+
}
90+
}
91+
3092
@SuppressWarnings("unchecked")
3193
public void testDataStreamLifecycleToXContent() throws Exception {
3294
TimeValue configuredRetention = TimeValue.timeValueDays(100);
3395
TimeValue globalDefaultRetention = TimeValue.timeValueDays(10);
3496
TimeValue globalMaxRetention = TimeValue.timeValueDays(50);
97+
DataStreamGlobalRetention globalRetention = new DataStreamGlobalRetention(globalDefaultRetention, globalMaxRetention);
3598
DataStreamLifecycle lifecycle = new DataStreamLifecycle(new DataStreamLifecycle.Retention(configuredRetention), null, null);
3699
{
37100
boolean isInternalDataStream = true;
38101
GetDataStreamLifecycleAction.Response.DataStreamLifecycle explainIndexDataStreamLifecycle = createDataStreamLifecycle(
39102
lifecycle,
40103
isInternalDataStream
41104
);
42-
Map<String, Object> resultMap = getXContentMap(explainIndexDataStreamLifecycle, globalDefaultRetention, globalMaxRetention);
105+
Map<String, Object> resultMap = getXContentMap(explainIndexDataStreamLifecycle, globalRetention);
43106
Map<String, Object> lifecycleResult = (Map<String, Object>) resultMap.get("lifecycle");
44107
assertThat(lifecycleResult.get("data_retention"), equalTo(configuredRetention.getStringRep()));
45108
assertThat(lifecycleResult.get("effective_retention"), equalTo(configuredRetention.getStringRep()));
@@ -51,7 +114,7 @@ public void testDataStreamLifecycleToXContent() throws Exception {
51114
lifecycle,
52115
isInternalDataStream
53116
);
54-
Map<String, Object> resultMap = getXContentMap(explainIndexDataStreamLifecycle, globalDefaultRetention, globalMaxRetention);
117+
Map<String, Object> resultMap = getXContentMap(explainIndexDataStreamLifecycle, globalRetention);
55118
Map<String, Object> lifecycleResult = (Map<String, Object>) resultMap.get("lifecycle");
56119
assertThat(lifecycleResult.get("data_retention"), equalTo(configuredRetention.getStringRep()));
57120
assertThat(lifecycleResult.get("effective_retention"), equalTo(globalMaxRetention.getStringRep()));
@@ -71,14 +134,11 @@ private GetDataStreamLifecycleAction.Response.DataStreamLifecycle createDataStre
71134
*/
72135
private Map<String, Object> getXContentMap(
73136
GetDataStreamLifecycleAction.Response.DataStreamLifecycle dataStreamLifecycle,
74-
TimeValue globalDefaultRetention,
75-
TimeValue globalMaxRetention
137+
DataStreamGlobalRetention globalRetention
76138
) throws IOException {
77139
try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) {
78-
ToXContent.Params params = new ToXContent.MapParams(Map.of(PATH_RESTRICTED, "serverless"));
79140
RolloverConfiguration rolloverConfiguration = null;
80-
DataStreamGlobalRetention globalRetention = new DataStreamGlobalRetention(globalDefaultRetention, globalMaxRetention);
81-
dataStreamLifecycle.toXContent(builder, params, rolloverConfiguration, globalRetention);
141+
dataStreamLifecycle.toXContent(builder, ToXContent.EMPTY_PARAMS, rolloverConfiguration, globalRetention);
82142
String serialized = Strings.toString(builder);
83143
return XContentHelper.convertToMap(XContentType.JSON.xContent(), serialized, randomBoolean());
84144
}

0 commit comments

Comments
 (0)