Skip to content

Commit dce2ef9

Browse files
authored
Write deprecation logs to a data stream (#61484)
Closes #46106. Implement a new log4j appender for deprecation logging, in order to write logs to a dedicated data stream. This is controlled by a new setting, `cluster.deprecation_indexing.enabled`.
1 parent 81d620b commit dce2ef9

File tree

14 files changed

+768
-304
lines changed

14 files changed

+768
-304
lines changed

server/src/main/java/org/elasticsearch/common/logging/DeprecatedMessage.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*/
3030
public class DeprecatedMessage {
3131
public static final String X_OPAQUE_ID_FIELD_NAME = "x-opaque-id";
32+
private static final String ECS_VERSION = "1.6";
3233

3334
@SuppressLoggerChecks(reason = "safely delegates to logger")
3435
public static ESLogMessage of(String key, String xOpaqueId, String messagePattern, Object... args){
@@ -40,10 +41,14 @@ public static ESLogMessage of(String key, String xOpaqueId, String messagePatter
4041
@Override
4142
public String toString() {
4243
return ParameterizedMessage.format(messagePattern, args);
43-
4444
}
4545
};
46+
4647
return new ESLogMessage(messagePattern, args)
48+
.field("data_stream.type", "logs")
49+
.field("data_stream.datatype", "deprecation")
50+
.field("data_stream.namespace", "elasticsearch")
51+
.field("ecs.version", ECS_VERSION)
4752
.field("key", key)
4853
.field("message", value)
4954
.field(X_OPAQUE_ID_FIELD_NAME, xOpaqueId);

x-pack/plugin/deprecation/build.gradle

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
apply plugin: 'elasticsearch.esplugin'
2-
apply plugin: 'elasticsearch.internal-cluster-test'
32

43
esplugin {
54
name 'x-pack-deprecation'
@@ -9,6 +8,15 @@ esplugin {
98
}
109
archivesBaseName = 'x-pack-deprecation'
1110

11+
// add all sub-projects of the qa sub-project
12+
gradle.projectsEvaluated {
13+
project.subprojects
14+
.find { it.path == project.path + ":qa" }
15+
.subprojects
16+
.findAll { it.path.startsWith(project.path + ":qa") }
17+
.each { check.dependsOn it.check }
18+
}
19+
1220
dependencies {
1321
compileOnly project(":x-pack:plugin:core")
1422
}

x-pack/plugin/deprecation/qa/build.gradle

Whitespace-only changes.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
apply plugin: 'elasticsearch.esplugin'
2+
apply plugin: 'elasticsearch.java-rest-test'
3+
4+
esplugin {
5+
description 'Deprecated query plugin'
6+
classname 'org.elasticsearch.xpack.deprecation.TestDeprecationPlugin'
7+
}
8+
9+
dependencies {
10+
javaRestTestImplementation("com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}")
11+
javaRestTestImplementation("com.fasterxml.jackson.core:jackson-databind:${versions.jackson}")
12+
// let the javaRestTest see the classpath of main
13+
javaRestTestImplementation project.sourceSets.main.runtimeClasspath
14+
}
15+
16+
restResources {
17+
restApi {
18+
includeCore '_common', 'indices', 'index'
19+
}
20+
}
21+
22+
testClusters.all {
23+
testDistribution = 'DEFAULT'
24+
setting 'xpack.security.enabled', 'false'
25+
}
26+
27+
test.enabled = false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
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+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.xpack.deprecation;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import org.apache.http.Header;
11+
import org.apache.http.HttpEntity;
12+
import org.apache.http.HttpHost;
13+
import org.apache.http.entity.ContentType;
14+
import org.apache.http.entity.StringEntity;
15+
import org.elasticsearch.client.Request;
16+
import org.elasticsearch.client.Response;
17+
import org.elasticsearch.client.RestClient;
18+
import org.elasticsearch.client.RestClientBuilder;
19+
import org.elasticsearch.common.Strings;
20+
import org.elasticsearch.common.logging.HeaderWarning;
21+
import org.elasticsearch.common.logging.LoggerMessageFormat;
22+
import org.elasticsearch.common.settings.Setting;
23+
import org.elasticsearch.common.settings.Settings;
24+
import org.elasticsearch.common.xcontent.XContentBuilder;
25+
import org.elasticsearch.common.xcontent.json.JsonXContent;
26+
import org.elasticsearch.test.rest.ESRestTestCase;
27+
import org.hamcrest.Matcher;
28+
29+
import java.io.IOException;
30+
import java.util.ArrayList;
31+
import java.util.Collections;
32+
import java.util.HashMap;
33+
import java.util.List;
34+
import java.util.Map;
35+
import java.util.stream.Collectors;
36+
37+
import static org.elasticsearch.test.hamcrest.RegexMatcher.matches;
38+
import static org.hamcrest.Matchers.containsString;
39+
import static org.hamcrest.Matchers.equalTo;
40+
import static org.hamcrest.Matchers.greaterThan;
41+
import static org.hamcrest.Matchers.hasEntry;
42+
import static org.hamcrest.Matchers.hasItem;
43+
import static org.hamcrest.Matchers.hasItems;
44+
import static org.hamcrest.Matchers.hasSize;
45+
46+
/**
47+
* Tests {@code DeprecationLogger} uses the {@code ThreadContext} to add response headers.
48+
*/
49+
public class DeprecationHttpIT extends ESRestTestCase {
50+
51+
/**
52+
* Check that configuring deprecation settings causes a warning to be added to the
53+
* response headers.
54+
*/
55+
public void testDeprecatedSettingsReturnWarnings() throws IOException {
56+
XContentBuilder builder = JsonXContent.contentBuilder()
57+
.startObject()
58+
.startObject("transient")
59+
.field(
60+
TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1.getKey(),
61+
!TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1.getDefault(Settings.EMPTY)
62+
)
63+
.field(
64+
TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE2.getKey(),
65+
!TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE2.getDefault(Settings.EMPTY)
66+
)
67+
// There should be no warning for this field
68+
.field(
69+
TestDeprecationHeaderRestAction.TEST_NOT_DEPRECATED_SETTING.getKey(),
70+
!TestDeprecationHeaderRestAction.TEST_NOT_DEPRECATED_SETTING.getDefault(Settings.EMPTY)
71+
)
72+
.endObject()
73+
.endObject();
74+
75+
final Request request = new Request("PUT", "_cluster/settings");
76+
request.setJsonEntity(Strings.toString(builder));
77+
final Response response = client().performRequest(request);
78+
79+
final List<String> deprecatedWarnings = getWarningHeaders(response.getHeaders());
80+
final List<Matcher<String>> headerMatchers = new ArrayList<>(2);
81+
82+
for (Setting<Boolean> setting : List.of(
83+
TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1,
84+
TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE2
85+
)) {
86+
headerMatchers.add(
87+
equalTo(
88+
"["
89+
+ setting.getKey()
90+
+ "] setting was deprecated in Elasticsearch and will be removed in a future release! "
91+
+ "See the breaking changes documentation for the next major version."
92+
)
93+
);
94+
}
95+
96+
assertThat(deprecatedWarnings, hasSize(headerMatchers.size()));
97+
for (final String deprecatedWarning : deprecatedWarnings) {
98+
assertThat(
99+
"Header does not conform to expected pattern",
100+
deprecatedWarning,
101+
matches(HeaderWarning.WARNING_HEADER_PATTERN.pattern())
102+
);
103+
}
104+
105+
final List<String> actualWarningValues = deprecatedWarnings.stream()
106+
.map(s -> HeaderWarning.extractWarningValueFromWarningHeader(s, true))
107+
.collect(Collectors.toList());
108+
for (Matcher<String> headerMatcher : headerMatchers) {
109+
assertThat(actualWarningValues, hasItem(headerMatcher));
110+
}
111+
}
112+
113+
/**
114+
* Attempts to do a scatter/gather request that expects unique responses per sub-request.
115+
*/
116+
@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/19222")
117+
public void testUniqueDeprecationResponsesMergedTogether() throws IOException {
118+
final String[] indices = new String[randomIntBetween(2, 5)];
119+
120+
// add at least one document for each index
121+
for (int i = 0; i < indices.length; ++i) {
122+
indices[i] = "test" + i;
123+
124+
// create indices with a single shard to reduce noise; the query only deprecates uniquely by index anyway
125+
createIndex(indices[i], Settings.builder().put("number_of_shards", 1).build());
126+
127+
int randomDocCount = randomIntBetween(1, 2);
128+
129+
for (int j = 0; j < randomDocCount; j++) {
130+
final Request request = new Request("PUT", indices[i] + "/" + j);
131+
request.setJsonEntity("{ \"field\": " + j + " }");
132+
assertOK(client().performRequest(request));
133+
}
134+
}
135+
136+
final String commaSeparatedIndices = String.join(",", indices);
137+
138+
client().performRequest(new Request("POST", commaSeparatedIndices + "/_refresh"));
139+
140+
// trigger all index deprecations
141+
Request request = new Request("GET", "/" + commaSeparatedIndices + "/_search");
142+
request.setJsonEntity("{ \"query\": { \"bool\": { \"filter\": [ { \"deprecated\": {} } ] } } }");
143+
Response response = client().performRequest(request);
144+
assertOK(response);
145+
146+
final List<String> deprecatedWarnings = getWarningHeaders(response.getHeaders());
147+
final List<Matcher<String>> headerMatchers = new ArrayList<>();
148+
149+
for (String index : indices) {
150+
headerMatchers.add(containsString(LoggerMessageFormat.format("[{}] index", (Object) index)));
151+
}
152+
153+
assertThat(deprecatedWarnings, hasSize(headerMatchers.size()));
154+
for (Matcher<String> headerMatcher : headerMatchers) {
155+
assertThat(deprecatedWarnings, hasItem(headerMatcher));
156+
}
157+
}
158+
159+
public void testDeprecationWarningsAppearInHeaders() throws Exception {
160+
doTestDeprecationWarningsAppearInHeaders();
161+
}
162+
163+
public void testDeprecationHeadersDoNotGetStuck() throws Exception {
164+
doTestDeprecationWarningsAppearInHeaders();
165+
doTestDeprecationWarningsAppearInHeaders();
166+
if (rarely()) {
167+
doTestDeprecationWarningsAppearInHeaders();
168+
}
169+
}
170+
171+
/**
172+
* Run a request that receives a predictably randomized number of deprecation warnings.
173+
* <p>
174+
* Re-running this back-to-back helps to ensure that warnings are not being maintained across requests.
175+
*/
176+
private void doTestDeprecationWarningsAppearInHeaders() throws IOException {
177+
final boolean useDeprecatedField = randomBoolean();
178+
final boolean useNonDeprecatedSetting = randomBoolean();
179+
180+
// deprecated settings should also trigger a deprecation warning
181+
final List<Setting<Boolean>> settings = new ArrayList<>(3);
182+
settings.add(TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1);
183+
184+
if (randomBoolean()) {
185+
settings.add(TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE2);
186+
}
187+
188+
if (useNonDeprecatedSetting) {
189+
settings.add(TestDeprecationHeaderRestAction.TEST_NOT_DEPRECATED_SETTING);
190+
}
191+
192+
Collections.shuffle(settings, random());
193+
194+
// trigger all deprecations
195+
Request request = new Request("GET", "/_test_cluster/deprecated_settings");
196+
request.setEntity(buildSettingsRequest(settings, useDeprecatedField));
197+
Response response = client().performRequest(request);
198+
assertOK(response);
199+
200+
final List<String> deprecatedWarnings = getWarningHeaders(response.getHeaders());
201+
final List<Matcher<String>> headerMatchers = new ArrayList<>(4);
202+
203+
headerMatchers.add(equalTo(TestDeprecationHeaderRestAction.DEPRECATED_ENDPOINT));
204+
if (useDeprecatedField) {
205+
headerMatchers.add(equalTo(TestDeprecationHeaderRestAction.DEPRECATED_USAGE));
206+
}
207+
208+
assertThat(deprecatedWarnings, hasSize(headerMatchers.size()));
209+
for (final String deprecatedWarning : deprecatedWarnings) {
210+
assertThat(deprecatedWarning, matches(HeaderWarning.WARNING_HEADER_PATTERN.pattern()));
211+
}
212+
final List<String> actualWarningValues = deprecatedWarnings.stream()
213+
.map(s -> HeaderWarning.extractWarningValueFromWarningHeader(s, true))
214+
.collect(Collectors.toList());
215+
for (Matcher<String> headerMatcher : headerMatchers) {
216+
assertThat(actualWarningValues, hasItem(headerMatcher));
217+
}
218+
}
219+
220+
/**
221+
* Check that deprecation messages can be recorded to an index
222+
*/
223+
public void testDeprecationMessagesCanBeIndexed() throws Exception {
224+
try {
225+
configureWriteDeprecationLogsToIndex(true);
226+
227+
Request request = new Request("GET", "/_test_cluster/deprecated_settings");
228+
request.setEntity(buildSettingsRequest(List.of(TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1), true));
229+
assertOK(client().performRequest(request));
230+
231+
assertBusy(() -> {
232+
Response response;
233+
try {
234+
response = client().performRequest(new Request("GET", "logs-deprecation-elasticsearch/_search"));
235+
} catch (Exception e) {
236+
// It can take a moment for the index to be created. If it doesn't exist then the client
237+
// throws an exception. Translate it into an assertion error so that assertBusy() will
238+
// continue trying.
239+
throw new AssertionError(e);
240+
}
241+
assertOK(response);
242+
243+
ObjectMapper mapper = new ObjectMapper();
244+
final JsonNode jsonNode = mapper.readTree(response.getEntity().getContent());
245+
246+
final int hits = jsonNode.at("/hits/total/value").intValue();
247+
assertThat(hits, greaterThan(0));
248+
249+
List<Map<String, Object>> documents = new ArrayList<>();
250+
251+
for (int i = 0; i < hits; i++) {
252+
final JsonNode hit = jsonNode.at("/hits/hits/" + i + "/_source");
253+
254+
final Map<String, Object> document = new HashMap<>();
255+
hit.fields().forEachRemaining(entry -> document.put(entry.getKey(), entry.getValue().textValue()));
256+
257+
documents.add(document);
258+
}
259+
260+
logger.warn(documents);
261+
assertThat(documents, hasSize(2));
262+
263+
assertThat(
264+
documents,
265+
hasItems(
266+
hasEntry("message", "[deprecated_settings] usage is deprecated. use [settings] instead"),
267+
hasEntry("message", "[/_test_cluster/deprecated_settings] exists for deprecated tests")
268+
)
269+
);
270+
});
271+
} finally {
272+
configureWriteDeprecationLogsToIndex(null);
273+
client().performRequest(new Request("DELETE", "_data_stream/logs-deprecation-elasticsearch"));
274+
}
275+
}
276+
277+
private void configureWriteDeprecationLogsToIndex(Boolean value) throws IOException {
278+
final Request request = new Request("PUT", "_cluster/settings");
279+
request.setJsonEntity("{ \"transient\": { \"cluster.deprecation_indexing.enabled\": " + value + " } }");
280+
final Response response = client().performRequest(request);
281+
assertOK(response);
282+
}
283+
284+
private List<String> getWarningHeaders(Header[] headers) {
285+
List<String> warnings = new ArrayList<>();
286+
287+
for (Header header : headers) {
288+
if (header.getName().equals("Warning")) {
289+
warnings.add(header.getValue());
290+
}
291+
}
292+
293+
return warnings;
294+
}
295+
296+
private HttpEntity buildSettingsRequest(List<Setting<Boolean>> settings, boolean useDeprecatedField) throws IOException {
297+
XContentBuilder builder = JsonXContent.contentBuilder();
298+
299+
builder.startObject().startArray(useDeprecatedField ? "deprecated_settings" : "settings");
300+
301+
for (Setting<Boolean> setting : settings) {
302+
builder.value(setting.getKey());
303+
}
304+
305+
builder.endArray().endObject();
306+
307+
return new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON);
308+
}
309+
310+
/**
311+
* Builds a REST client that will tolerate warnings in the response headers. The default
312+
* is to throw an exception.
313+
*/
314+
@Override
315+
protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOException {
316+
RestClientBuilder builder = RestClient.builder(hosts);
317+
configureClient(builder, settings);
318+
builder.setStrictDeprecationMode(false);
319+
return builder.build();
320+
}
321+
}

0 commit comments

Comments
 (0)