diff --git a/distribution/docker/src/docker/config/log4j2.properties b/distribution/docker/src/docker/config/log4j2.properties index 94701d312686d..d2616d6060a2f 100644 --- a/distribution/docker/src/docker/config/log4j2.properties +++ b/distribution/docker/src/docker/config/log4j2.properties @@ -12,9 +12,18 @@ appender.deprecation_rolling.type = Console appender.deprecation_rolling.name = deprecation_rolling appender.deprecation_rolling.layout.type = ECSJsonLayout appender.deprecation_rolling.layout.type_name = deprecation +appender.deprecation_rolling.filter.rate_limit.type = RateLimitingFilter + +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false logger.deprecation.name = org.elasticsearch.deprecation -logger.deprecation.level = warn +logger.deprecation.level = deprecation logger.deprecation.appenderRef.deprecation_rolling.ref = deprecation_rolling logger.deprecation.additivity = false diff --git a/distribution/docker/src/docker/config/oss/log4j2.properties b/distribution/docker/src/docker/config/oss/log4j2.properties index a4e175edf431d..7286142a6350e 100644 --- a/distribution/docker/src/docker/config/oss/log4j2.properties +++ b/distribution/docker/src/docker/config/oss/log4j2.properties @@ -8,13 +8,22 @@ appender.rolling.layout.type_name = server rootLogger.level = info rootLogger.appenderRef.rolling.ref = rolling +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false + appender.deprecation_rolling.type = Console appender.deprecation_rolling.name = deprecation_rolling appender.deprecation_rolling.layout.type = ECSJsonLayout appender.deprecation_rolling.layout.type_name = deprecation +appender.deprecation_rolling.filter.rate_limit.type = RateLimitingFilter logger.deprecation.name = org.elasticsearch.deprecation -logger.deprecation.level = warn +logger.deprecation.level = deprecation logger.deprecation.appenderRef.deprecation_rolling.ref = deprecation_rolling logger.deprecation.additivity = false diff --git a/distribution/src/config/log4j2.properties b/distribution/src/config/log4j2.properties index 2aac6f58dc4e6..28bd21030d01e 100644 --- a/distribution/src/config/log4j2.properties +++ b/distribution/src/config/log4j2.properties @@ -63,6 +63,7 @@ appender.deprecation_rolling.name = deprecation_rolling appender.deprecation_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation.json appender.deprecation_rolling.layout.type = ECSJsonLayout appender.deprecation_rolling.layout.type_name = deprecation +appender.deprecation_rolling.filter.rate_limit.type = RateLimitingFilter appender.deprecation_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation-%i.json.gz appender.deprecation_rolling.policies.type = Policies @@ -70,10 +71,18 @@ appender.deprecation_rolling.policies.size.type = SizeBasedTriggeringPolicy appender.deprecation_rolling.policies.size.size = 1GB appender.deprecation_rolling.strategy.type = DefaultRolloverStrategy appender.deprecation_rolling.strategy.max = 4 + +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false ################################################# logger.deprecation.name = org.elasticsearch.deprecation -logger.deprecation.level = warn +logger.deprecation.level = deprecation logger.deprecation.appenderRef.deprecation_rolling.ref = deprecation_rolling logger.deprecation.additivity = false diff --git a/libs/x-content/src/main/resources/log4j2.properties b/libs/x-content/src/main/resources/log4j2.properties new file mode 100644 index 0000000000000..f21ca54eccc6d --- /dev/null +++ b/libs/x-content/src/main/resources/log4j2.properties @@ -0,0 +1,12 @@ +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false + +logger.deprecation.name = deprecation +logger.deprecation.level = deprecation +logger.deprecation.appenderRef.deprecation_file.ref = deprecation_file +logger.deprecation.additivity = false diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java index fdd64ef1e73d1..d0d5f062cf6ef 100644 --- a/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java +++ b/qa/evil-tests/src/test/java/org/elasticsearch/common/logging/EvilLoggerTests.java @@ -57,6 +57,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static org.elasticsearch.common.logging.DeprecationLogger.DEPRECATION; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -119,7 +120,7 @@ public void testConcurrentDeprecationLogger() throws IOException, UserException, final List ids = IntStream.range(0, 128).boxed().collect(Collectors.toList()); Randomness.shuffle(ids); final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - DeprecationLogger.setThreadContext(threadContext); + HeaderWarning.setThreadContext(threadContext); try { barrier.await(); } catch (final BrokenBarrierException | InterruptedException e) { @@ -180,8 +181,8 @@ public void testConcurrentDeprecationLogger() throws IOException, UserException, for (int i = 0; i < 128; i++) { assertLogLine( deprecationEvents.get(i), - Level.WARN, - "org.elasticsearch.common.logging.ThrottlingLogger\\$2\\.run", + DEPRECATION, + "org.elasticsearch.common.logging.DeprecationLogger\\$DeprecationLoggerBuilder.withDeprecation", "This is a maybe logged deprecation message" + i); } @@ -191,49 +192,6 @@ public void testConcurrentDeprecationLogger() throws IOException, UserException, } - public void testDeprecationLoggerMaybeLog() throws IOException, UserException { - setupLogging("deprecation"); - - final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger("deprecation"); - - final int iterations = randomIntBetween(1, 16); - - for (int i = 0; i < iterations; i++) { - deprecationLogger.deprecate("key", "This is a maybe logged deprecation message"); - assertWarnings("This is a maybe logged deprecation message"); - } - for (int k = 0; k < 128; k++) { - for (int i = 0; i < iterations; i++) { - deprecationLogger.deprecate("key" + k, "This is a maybe logged deprecation message" + k); - assertWarnings("This is a maybe logged deprecation message" + k); - } - } - for (int i = 0; i < iterations; i++) { - deprecationLogger.deprecate("key", "This is a maybe logged deprecation message"); - assertWarnings("This is a maybe logged deprecation message"); - } - - final String deprecationPath = - System.getProperty("es.logs.base_path") + - System.getProperty("file.separator") + - System.getProperty("es.logs.cluster_name") + - "_deprecation.log"; - final List deprecationEvents = Files.readAllLines(PathUtils.get(deprecationPath)); - assertThat(deprecationEvents.size(), equalTo(1 + 128 + 1)); - assertLogLine( - deprecationEvents.get(0), - Level.WARN, - "org.elasticsearch.common.logging.ThrottlingLogger\\$2\\.run", - "This is a maybe logged deprecation message"); - for (int k = 0; k < 128; k++) { - assertLogLine( - deprecationEvents.get(1 + k), - Level.WARN, - "org.elasticsearch.common.logging.ThrottlingLogger\\$2\\.run", - "This is a maybe logged deprecation message" + k); - } - } - public void testDeprecatedSettings() throws IOException, UserException { setupLogging("settings"); @@ -256,8 +214,8 @@ public void testDeprecatedSettings() throws IOException, UserException { assertThat(deprecationEvents.size(), equalTo(1)); assertLogLine( deprecationEvents.get(0), - Level.WARN, - "org.elasticsearch.common.logging.ThrottlingLogger\\$2\\.run", + DEPRECATION, + "org.elasticsearch.common.logging.DeprecationLogger\\$DeprecationLoggerBuilder.withDeprecation", "\\[deprecated.foo\\] setting was deprecated in Elasticsearch and will be removed in a future release! " + "See the breaking changes documentation for the next major version."); } diff --git a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/config/log4j2.properties b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/config/log4j2.properties index 2c9f48a359a46..1bd5013729d46 100644 --- a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/config/log4j2.properties +++ b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/config/log4j2.properties @@ -25,6 +25,14 @@ appender.deprecation_file.fileName = ${sys:es.logs.base_path}${sys:file.separato appender.deprecation_file.layout.type = PatternLayout appender.deprecation_file.layout.pattern = [%p][%l] [%test_thread_info]%marker %m%n +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false + logger.deprecation.name = deprecation logger.deprecation.level = warn logger.deprecation.appenderRef.deprecation_file.ref = deprecation_file diff --git a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/deprecation/log4j2.properties b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/deprecation/log4j2.properties index 388c9f9b2fc64..e19735eaf92f3 100644 --- a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/deprecation/log4j2.properties +++ b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/deprecation/log4j2.properties @@ -18,8 +18,17 @@ appender.deprecation_file.name = deprecation_file appender.deprecation_file.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation.log appender.deprecation_file.layout.type = PatternLayout appender.deprecation_file.layout.pattern = [%p][%l] [%test_thread_info]%marker %m%n +appender.deprecation_file.filter.rate_limit.type = RateLimitingFilter + +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false logger.deprecation.name = deprecation -logger.deprecation.level = warn +logger.deprecation.level = deprecation logger.deprecation.appenderRef.deprecation_file.ref = deprecation_file logger.deprecation.additivity = false diff --git a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/no_node_name/log4j2.properties b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/no_node_name/log4j2.properties index fd7af2ce73136..cd723924eaddc 100644 --- a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/no_node_name/log4j2.properties +++ b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/no_node_name/log4j2.properties @@ -19,6 +19,14 @@ appender.deprecation_file.fileName = ${sys:es.logs.base_path}${sys:file.separato appender.deprecation_file.layout.type = PatternLayout appender.deprecation_file.layout.pattern = [%p][%l] %marker%m%n +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false + logger.deprecation.name = deprecation logger.deprecation.level = warn logger.deprecation.appenderRef.deprecation_file.ref = deprecation_file diff --git a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/settings/log4j2.properties b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/settings/log4j2.properties index abe4a279dc820..c9977acacb383 100644 --- a/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/settings/log4j2.properties +++ b/qa/evil-tests/src/test/resources/org/elasticsearch/common/logging/settings/log4j2.properties @@ -18,9 +18,18 @@ appender.deprecation_file.name = deprecation_file appender.deprecation_file.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecation.log appender.deprecation_file.layout.type = PatternLayout appender.deprecation_file.layout.pattern = [%p][%l] [%test_thread_info]%marker %m%n +appender.deprecation_file.filter.rate_limit.type = RateLimitingFilter + +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false logger.deprecation.name = org.elasticsearch.deprecation.common.settings -logger.deprecation.level = warn +logger.deprecation.level = deprecation logger.deprecation.appenderRef.deprecation_console.ref = console logger.deprecation.appenderRef.deprecation_file.ref = deprecation_file logger.deprecation.additivity = false diff --git a/qa/logging-config/custom-log4j2.properties b/qa/logging-config/custom-log4j2.properties index be4ffc03f20ef..7164264da7654 100644 --- a/qa/logging-config/custom-log4j2.properties +++ b/qa/logging-config/custom-log4j2.properties @@ -87,8 +87,15 @@ appender.deprecation_rolling_old.policies.size.size = 1GB appender.deprecation_rolling_old.strategy.type = DefaultRolloverStrategy appender.deprecation_rolling_old.strategy.max = 4 ################################################# +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false +################################################# logger.deprecation.name = org.elasticsearch.deprecation -logger.deprecation.level = warn +logger.deprecation.level = deprecation logger.deprecation.appenderRef.deprecation_rolling.ref = deprecation_rolling logger.deprecation.appenderRef.deprecation_rolling_old.ref = deprecation_rolling_old logger.deprecation.additivity = false diff --git a/qa/logging-config/src/test/java/org/elasticsearch/common/logging/JsonLoggerTests.java b/qa/logging-config/src/test/java/org/elasticsearch/common/logging/JsonLoggerTests.java index c2a467212c084..93ca01f446878 100644 --- a/qa/logging-config/src/test/java/org/elasticsearch/common/logging/JsonLoggerTests.java +++ b/qa/logging-config/src/test/java/org/elasticsearch/common/logging/JsonLoggerTests.java @@ -48,6 +48,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.elasticsearch.common.logging.DeprecationLogger.DEPRECATION; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasEntry; @@ -86,11 +87,13 @@ public void tearDown() throws Exception { } public void testDeprecatedMessage() throws IOException { - final Logger testLogger = LogManager.getLogger("deprecation.test"); - testLogger.info(DeprecatedMessage.of("someId","deprecated message1")); + setXOpaqueId("someId"); + final DeprecationLogger testLogger = DeprecationLogger.getLogger("test"); + testLogger.deprecate("someKey", "deprecated message1"); final Path path = PathUtils.get(System.getProperty("es.logs.base_path"), System.getProperty("es.logs.cluster_name") + "_deprecated.json"); + try (Stream> stream = JsonLogsStream.mapStreamFrom(path)) { List> jsonLogs = stream .collect(Collectors.toList()); @@ -98,7 +101,7 @@ public void testDeprecatedMessage() throws IOException { assertThat(jsonLogs, contains( allOf( hasEntry("type", "deprecation"), - hasEntry("log.level", "INFO"), + hasEntry("log.level", "DEPRECATION"), hasEntry("log.logger", "deprecation.test"), hasEntry("cluster.name", "elasticsearch"), hasEntry("node.name", "sample-name"), @@ -107,6 +110,8 @@ public void testDeprecatedMessage() throws IOException { ) ); } + + assertWarnings("deprecated message1"); } public void testBuildingMessage() throws IOException { @@ -168,14 +173,17 @@ public void testCustomMessageWithMultipleFields() throws IOException { public void testDeprecatedMessageWithoutXOpaqueId() throws IOException { - final Logger testLogger = LogManager.getLogger("deprecation.test"); - testLogger.info( DeprecatedMessage.of("someId","deprecated message1")); - testLogger.info( DeprecatedMessage.of("","deprecated message2")); - testLogger.info( DeprecatedMessage.of(null,"deprecated message3")); - testLogger.info("deprecated message4"); + final DeprecationLogger testLogger = DeprecationLogger.getLogger("test"); + + testLogger.deprecate("a key", "deprecated message1"); + + // Also test that a message sent directly to the logger comes through + final Logger rawLogger = LogManager.getLogger("deprecation.test"); + rawLogger.log(DEPRECATION, "deprecated message2"); final Path path = PathUtils.get(System.getProperty("es.logs.base_path"), System.getProperty("es.logs.cluster_name") + "_deprecated.json"); + try (Stream> stream = JsonLogsStream.mapStreamFrom(path)) { List> jsonLogs = stream .collect(Collectors.toList()); @@ -183,42 +191,29 @@ public void testDeprecatedMessageWithoutXOpaqueId() throws IOException { assertThat(jsonLogs, contains( allOf( hasEntry("type", "deprecation"), - hasEntry("log.level", "INFO"), + hasEntry("log.level", "DEPRECATION"), hasEntry("log.logger", "deprecation.test"), hasEntry("cluster.name", "elasticsearch"), hasEntry("node.name", "sample-name"), hasEntry("message", "deprecated message1"), - hasEntry("x-opaque-id", "someId")), - allOf( - hasEntry("type", "deprecation"), - hasEntry("log.level", "INFO"), - hasEntry("log.logger", "deprecation.test"), - hasEntry("cluster.name", "elasticsearch"), - hasEntry("node.name", "sample-name"), - hasEntry("message", "deprecated message2"), - not(hasKey("x-opaque-id")) - ), - allOf( - hasEntry("type", "deprecation"), - hasEntry("log.level", "INFO"), - hasEntry("log.logger", "deprecation.test"), - hasEntry("cluster.name", "elasticsearch"), - hasEntry("node.name", "sample-name"), - hasEntry("message", "deprecated message3"), not(hasKey("x-opaque-id")) ), allOf( hasEntry("type", "deprecation"), - hasEntry("log.level", "INFO"), + hasEntry("log.level", "DEPRECATION"), hasEntry("log.logger", "deprecation.test"), hasEntry("cluster.name", "elasticsearch"), hasEntry("node.name", "sample-name"), - hasEntry("message", "deprecated message4"), + hasEntry("message", "deprecated message2"), not(hasKey("x-opaque-id")) ) ) ); } + + // The message sent directly to the logger does not appear in the header warnings, because + // it is DeprecationLogger that also writes it to the `header_logger` logger instance. + assertWarnings("deprecated message1"); } public void testJsonLayout() throws IOException { @@ -342,7 +337,7 @@ public void testDuplicateLogMessages() throws IOException { ThreadContext threadContext = new ThreadContext(Settings.EMPTY); try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { threadContext.putHeader(Task.X_OPAQUE_ID, "ID1"); - DeprecationLogger.setThreadContext(threadContext); + HeaderWarning.setThreadContext(threadContext); deprecationLogger.deprecate("key", "message1"); deprecationLogger.deprecate("key", "message2"); assertWarnings("message1", "message2"); @@ -356,7 +351,7 @@ public void testDuplicateLogMessages() throws IOException { assertThat(jsonLogs, contains( allOf( hasEntry("type", "deprecation"), - hasEntry("log.level", "WARN"), + hasEntry("log.level", "DEPRECATION"), hasEntry("log.logger", "deprecation.test"), hasEntry("cluster.name", "elasticsearch"), hasEntry("node.name", "sample-name"), @@ -366,7 +361,7 @@ public void testDuplicateLogMessages() throws IOException { ); } } finally { - DeprecationLogger.removeThreadContext(threadContext); + HeaderWarning.removeThreadContext(threadContext); } @@ -374,7 +369,7 @@ public void testDuplicateLogMessages() throws IOException { //continuing with message1-ID1 in logs already, adding a new deprecation log line with message2-ID2 try (ThreadContext.StoredContext ignore = threadContext.stashContext()) { threadContext.putHeader(Task.X_OPAQUE_ID, "ID2"); - DeprecationLogger.setThreadContext(threadContext); + HeaderWarning.setThreadContext(threadContext); deprecationLogger.deprecate("key", "message1"); deprecationLogger.deprecate("key", "message2"); assertWarnings("message1", "message2"); @@ -388,7 +383,7 @@ public void testDuplicateLogMessages() throws IOException { assertThat(jsonLogs, contains( allOf( hasEntry("type", "deprecation"), - hasEntry("log.level", "WARN"), + hasEntry("log.level", "DEPRECATION"), hasEntry("log.logger", "deprecation.test"), hasEntry("cluster.name", "elasticsearch"), hasEntry("node.name", "sample-name"), @@ -397,7 +392,7 @@ public void testDuplicateLogMessages() throws IOException { ), allOf( hasEntry("type", "deprecation"), - hasEntry("log.level", "WARN"), + hasEntry("log.level", "DEPRECATION"), hasEntry("log.logger", "deprecation.test"), hasEntry("cluster.name", "elasticsearch"), hasEntry("node.name", "sample-name"), @@ -408,7 +403,7 @@ public void testDuplicateLogMessages() throws IOException { ); } } finally { - DeprecationLogger.removeThreadContext(threadContext); + HeaderWarning.removeThreadContext(threadContext); } } diff --git a/qa/logging-config/src/test/resources/org/elasticsearch/common/logging/json_layout/log4j2.properties b/qa/logging-config/src/test/resources/org/elasticsearch/common/logging/json_layout/log4j2.properties index 46c91d4d89b8e..7bc87d663efe7 100644 --- a/qa/logging-config/src/test/resources/org/elasticsearch/common/logging/json_layout/log4j2.properties +++ b/qa/logging-config/src/test/resources/org/elasticsearch/common/logging/json_layout/log4j2.properties @@ -15,11 +15,13 @@ appender.deprecated.name = deprecated appender.deprecated.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_deprecated.json appender.deprecated.layout.type = ECSJsonLayout appender.deprecated.layout.type_name = deprecation +appender.deprecated.filter.rate_limit.type = RateLimitingFilter appender.deprecatedconsole.type = Console appender.deprecatedconsole.name = deprecatedconsole appender.deprecatedconsole.layout.type = ECSJsonLayout appender.deprecatedconsole.layout.type_name = deprecation +appender.deprecatedconsole.filter.rate_limit.type = RateLimitingFilter rootLogger.level = info @@ -30,10 +32,18 @@ appender.plaintext.name = plaintext appender.plaintext.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_plaintext.json appender.plaintext.layout.type = PatternLayout appender.plaintext.layout.pattern =%m%n +appender.plaintext.filter.rate_limit.type = RateLimitingFilter +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning +logger.header_logger.additivity = false logger.deprecation.name = deprecation.test -logger.deprecation.level = trace +logger.deprecation.level = deprecation logger.deprecation.appenderRef.deprecation_rolling.ref = deprecated logger.deprecation.appenderRef.deprecatedconsole.ref = deprecatedconsole logger.deprecation.additivity = false diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java index 8131b0e53e5c9..e1690264beb8f 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java @@ -41,7 +41,6 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.compress.CompressedXContent; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.settings.IndexScopedSettings; import org.elasticsearch.common.settings.Settings; @@ -97,6 +96,7 @@ public class MetadataIndexTemplateService { " }\n" + " }"; private static final Logger logger = LogManager.getLogger(MetadataIndexTemplateService.class); + private static final Logger headerLogger = LogManager.getLogger("header_logger"); private final ClusterService clusterService; private final AliasValidator aliasValidator; private final IndicesService indicesService; @@ -480,7 +480,7 @@ public ClusterState addIndexTemplateV2(final ClusterState currentState, final bo .collect(Collectors.joining(",")), name); logger.warn(warning); - HeaderWarning.addWarning(warning); + headerLogger.warn(warning); } ComposableIndexTemplate finalIndexTemplate = template; @@ -792,7 +792,7 @@ static ClusterState innerPutTemplate(final ClusterState currentState, PutRequest .collect(Collectors.joining(",")), request.name); logger.warn(warning); - HeaderWarning.addWarning(warning); + headerLogger.warn(warning); } else { // Otherwise, this is a hard error, the user should use V2 index templates instead String error = String.format(Locale.ROOT, "legacy template [%s] has index patterns %s matching patterns" + diff --git a/server/src/main/java/org/elasticsearch/common/logging/DeprecatedMessage.java b/server/src/main/java/org/elasticsearch/common/logging/DeprecatedMessage.java index 2cc882d61695a..b6d3cf3d7eedf 100644 --- a/server/src/main/java/org/elasticsearch/common/logging/DeprecatedMessage.java +++ b/server/src/main/java/org/elasticsearch/common/logging/DeprecatedMessage.java @@ -28,12 +28,12 @@ * Carries x-opaque-id field if provided in the headers. Will populate the x-opaque-id field in JSON logs. */ public class DeprecatedMessage { - private static final String X_OPAQUE_ID_FIELD_NAME = "x-opaque-id"; + public static final String X_OPAQUE_ID_FIELD_NAME = "x-opaque-id"; @SuppressLoggerChecks(reason = "safely delegates to logger") - public static ESLogMessage of(String xOpaqueId, String messagePattern, Object... args){ + public static ESLogMessage of(String key, String xOpaqueId, String messagePattern, Object... args){ if (Strings.isNullOrEmpty(xOpaqueId)) { - return new ESLogMessage(messagePattern, args); + return new ESLogMessage(messagePattern, args).field("key", key); } Object value = new Object() { @@ -44,6 +44,7 @@ public String toString() { } }; return new ESLogMessage(messagePattern, args) + .field("key", key) .field("message", value) .field(X_OPAQUE_ID_FIELD_NAME, xOpaqueId); } diff --git a/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java b/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java index 3a5abf622b0cf..6b94cca01a98e 100644 --- a/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java +++ b/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java @@ -19,45 +19,57 @@ package org.elasticsearch.common.logging; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.elasticsearch.common.util.concurrent.ThreadContext; /** * A logger that logs deprecation notices. Logger should be initialized with a parent logger which name will be used - * for deprecation logger. For instance new DeprecationLogger("org.elasticsearch.test.SomeClass") will - * result in a deprecation logger with name org.elasticsearch.deprecation.test.SomeClass. This allows to use + * for deprecation logger. For instance DeprecationLogger.getLogger("org.elasticsearch.test.SomeClass") will + * result in a deprecation logger with name org.elasticsearch.deprecation.test.SomeClass. This allows to use a * deprecation logger defined in log4j2.properties. - * - * Deprecation logs are written to deprecation log file - defined in log4j2.properties, as well as warnings added to a response header. - * All deprecation usages are throttled basing on a key. Key is a string provided in an argument and can be prefixed with - * X-Opaque-Id. This allows to throttle deprecations per client usage. - * deprecationLogger.deprecate("key","message {}", "param"); - * - * @see ThrottlingAndHeaderWarningLogger for throttling and header warnings implementation details + *

+ * Logs are emitted at the custom {@link #DEPRECATION} level, and routed wherever they need to go using log4j. For example, + * to disk using a rolling file appender, or added as a response header using {@link HeaderWarningAppender}. + *

+ * Deprecation messages include a key, which is used for rate-limiting purposes. The log4j configuration + * uses {@link RateLimitingFilter} to prevent the same message being logged repeatedly in a short span of time. This + * key is combined with the X-Opaque-Id request header value, if supplied, which allows for per-client + * message limiting. */ public class DeprecationLogger { - private final ThrottlingAndHeaderWarningLogger deprecationLogger; /** - * Creates a new deprecation logger based on the parent logger. Automatically - * prefixes the logger name with "deprecation", if it starts with "org.elasticsearch.", - * it replaces "org.elasticsearch" with "org.elasticsearch.deprecation" to maintain - * the "org.elasticsearch" namespace. + * Deprecation messages are logged at this level. */ + public static Level DEPRECATION = Level.forName("DEPRECATION", Level.WARN.intLevel() + 1); + + private final Logger logger; + private final Logger headerLogger = LogManager.getLogger("header_logger"); + private DeprecationLogger(Logger parentLogger) { - deprecationLogger = new ThrottlingAndHeaderWarningLogger(parentLogger); + this.logger = parentLogger; } + /** + * Creates a new deprecation logger for the supplied class. Internally, it delegates to + * {@link #getLogger(String)}, passing the full class name. + */ public static DeprecationLogger getLogger(Class aClass) { return getLogger(toLoggerName(aClass)); } + /** + * Creates a new deprecation logger based on the parent logger. Automatically + * prefixes the logger name with "deprecation", if it starts with "org.elasticsearch.", + * it replaces "org.elasticsearch" with "org.elasticsearch.deprecation" to maintain + * the "org.elasticsearch" namespace. + */ public static DeprecationLogger getLogger(String name) { - return new DeprecationLogger(deprecatedLoggerName(name)); + return new DeprecationLogger(getDeprecatedLoggerForName(name)); } - private static Logger deprecatedLoggerName(String name) { + private static Logger getDeprecatedLoggerForName(String name) { if (name.startsWith("org.elasticsearch")) { name = name.replace("org.elasticsearch.", "org.elasticsearch.deprecation."); } else { @@ -71,32 +83,23 @@ private static String toLoggerName(final Class cls) { return canonicalName != null ? canonicalName : cls.getName(); } - public static void setThreadContext(ThreadContext threadContext) { - HeaderWarning.setThreadContext(threadContext); - } - - public static void removeThreadContext(ThreadContext threadContext) { - HeaderWarning.removeThreadContext(threadContext); - } - /** - * Logs a deprecation message, adding a formatted warning message as a response header on the thread context. - * The deprecation message will be throttled to deprecation log. - * method returns a builder as more methods are expected to be chained. + * Logs a message at the {@link #DEPRECATION} level. The message is also sent to the header warning logger, + * so that it can be returned to the client. */ public DeprecationLoggerBuilder deprecate(final String key, final String msg, final Object... params) { - return new DeprecationLoggerBuilder() - .withDeprecation(key, msg, params); + return new DeprecationLoggerBuilder().withDeprecation(key, msg, params); } public class DeprecationLoggerBuilder { public DeprecationLoggerBuilder withDeprecation(String key, String msg, Object[] params) { - String opaqueId = HeaderWarning.getXOpaqueId(); - ESLogMessage deprecationMessage = DeprecatedMessage.of(opaqueId, msg, params); - deprecationLogger.throttleLogAndAddWarning(key, deprecationMessage); + ESLogMessage deprecationMessage = DeprecatedMessage.of(key, HeaderWarning.getXOpaqueId(), msg, params); + + logger.log(DEPRECATION, deprecationMessage); + headerLogger.warn(deprecationMessage); + return this; } - } } diff --git a/server/src/main/java/org/elasticsearch/common/logging/HeaderWarning.java b/server/src/main/java/org/elasticsearch/common/logging/HeaderWarning.java index 714c738a50e57..9b79930095d5b 100644 --- a/server/src/main/java/org/elasticsearch/common/logging/HeaderWarning.java +++ b/server/src/main/java/org/elasticsearch/common/logging/HeaderWarning.java @@ -77,7 +77,7 @@ public class HeaderWarning { Build.CURRENT.isSnapshot() ? "-SNAPSHOT" : "", Build.CURRENT.hash()); - private static BitSet doesNotNeedEncoding; + private static final BitSet doesNotNeedEncoding; static { doesNotNeedEncoding = new BitSet(1 + 0xFF); @@ -205,10 +205,7 @@ private static boolean assertWarningValue(final String s, final String warningVa */ public static String formatWarning(final String s) { // Assume that the common scenario won't have a string to escape and encode. - int length = WARNING_PREFIX.length() + s.length() + 3; - final StringBuilder sb = new StringBuilder(length); - sb.append(WARNING_PREFIX).append(" \"").append(escapeAndEncode(s)).append("\""); - return sb.toString(); + return WARNING_PREFIX + " \"" + escapeAndEncode(s) + "\""; } /** @@ -321,7 +318,7 @@ public static String getXOpaqueId() { .orElse(""); } - public static void addWarning(String message, Object... params) { + static void addWarning(String message, Object... params) { addWarning(THREAD_CONTEXT, message, params); } diff --git a/server/src/main/java/org/elasticsearch/common/logging/HeaderWarningAppender.java b/server/src/main/java/org/elasticsearch/common/logging/HeaderWarningAppender.java new file mode 100644 index 0000000000000..d793b31e69562 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/logging/HeaderWarningAppender.java @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.logging; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.message.Message; + +@Plugin(name = "HeaderWarningAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class HeaderWarningAppender extends AbstractAppender { + public HeaderWarningAppender(String name, Filter filter) { + super(name, filter, null); + } + + @Override + public void append(LogEvent event) { + final Message message = event.getMessage(); + + if (message instanceof ESLogMessage) { + final ESLogMessage esLogMessage = (ESLogMessage) message; + + String messagePattern = esLogMessage.getMessagePattern(); + Object[] arguments = esLogMessage.getArguments(); + + HeaderWarning.addWarning(messagePattern, arguments); + } else { + final String formattedMessage = event.getMessage().getFormattedMessage(); + HeaderWarning.addWarning(formattedMessage); + } + } + + @PluginFactory + public static HeaderWarningAppender createAppender( + @PluginAttribute("name") String name, + @PluginElement("filter") Filter filter + ) { + return new HeaderWarningAppender(name, filter); + } +} diff --git a/server/src/main/java/org/elasticsearch/common/logging/RateLimitingFilter.java b/server/src/main/java/org/elasticsearch/common/logging/RateLimitingFilter.java new file mode 100644 index 0000000000000..b477741b97693 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/logging/RateLimitingFilter.java @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.logging; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.filter.AbstractFilter; +import org.apache.logging.log4j.message.Message; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.common.logging.DeprecatedMessage.X_OPAQUE_ID_FIELD_NAME; + +@Plugin(name = "RateLimitingFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE) +public class RateLimitingFilter extends AbstractFilter { + + private final Set lruKeyCache = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap<>() { + @Override + protected boolean removeEldestEntry(final Map.Entry eldest) { + return size() > 128; + } + })); + + public RateLimitingFilter() { + this(Result.ACCEPT, Result.DENY); + } + + public RateLimitingFilter(Result onMatch, Result onMismatch) { + super(onMatch, onMismatch); + } + + /** + * Clears the cache of previously-seen keys. + */ + public void reset() { + this.lruKeyCache.clear(); + } + + public Result filter(Message message) { + if (message instanceof ESLogMessage) { + final ESLogMessage esLogMessage = (ESLogMessage) message; + + String xOpaqueId = esLogMessage.get(X_OPAQUE_ID_FIELD_NAME); + final String key = esLogMessage.get("key"); + + return lruKeyCache.add(xOpaqueId + key) ? Result.ACCEPT : Result.DENY; + + } else { + return Result.NEUTRAL; + } + } + + @Override + public Result filter(LogEvent event) { + return filter(event.getMessage()); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) { + return filter(msg); + } + + @PluginFactory + public static RateLimitingFilter createFilter( + @PluginAttribute("onMatch") final Result match, + @PluginAttribute("onMismatch") final Result mismatch + ) { + return new RateLimitingFilter(match, mismatch); + } +} diff --git a/server/src/main/java/org/elasticsearch/common/logging/ThrottlingAndHeaderWarningLogger.java b/server/src/main/java/org/elasticsearch/common/logging/ThrottlingAndHeaderWarningLogger.java deleted file mode 100644 index 0dec2b45d0244..0000000000000 --- a/server/src/main/java/org/elasticsearch/common/logging/ThrottlingAndHeaderWarningLogger.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.common.logging; - -import org.apache.logging.log4j.Logger; - -/** - * This class wraps both HeaderWarningLogger and ThrottlingLogger - * which is a common use case across Elasticsearch - */ -class ThrottlingAndHeaderWarningLogger { - private final ThrottlingLogger throttlingLogger; - - ThrottlingAndHeaderWarningLogger(Logger logger) { - this.throttlingLogger = new ThrottlingLogger(logger); - } - - /** - * Adds a formatted warning message as a response header on the thread context, and logs a message if the associated key has - * not recently been seen. - * - * @param key the key used to determine if this message should be logged - * @param message the message to log - */ - void throttleLogAndAddWarning(final String key, ESLogMessage message) { - String messagePattern = message.getMessagePattern(); - Object[] arguments = message.getArguments(); - HeaderWarning.addWarning(messagePattern, arguments); - throttlingLogger.throttleLog(key, message); - } - -} diff --git a/server/src/main/java/org/elasticsearch/common/logging/ThrottlingLogger.java b/server/src/main/java/org/elasticsearch/common/logging/ThrottlingLogger.java deleted file mode 100644 index 072f7c5c05a0c..0000000000000 --- a/server/src/main/java/org/elasticsearch/common/logging/ThrottlingLogger.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.elasticsearch.common.logging; - -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.Message; -import org.elasticsearch.common.SuppressLoggerChecks; - -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -/** - * TODO wrapping logging this way limits the usage of %location. It will think this is used from that class. - *

- * This is a wrapper around a logger that allows to throttle log messages. - * In order to throttle a key has to be used and throttling happens per each key combined with X-Opaque-Id. - * X-Opaque-Id allows throttling per user. This value is set in ThreadContext from X-Opaque-Id HTTP header. - *

- * The throttling algorithm is relying on LRU set of keys which evicts entries when its size is > 128. - * When a log with a key is emitted, it won't be logged again until the set reaches size 128 and the key is removed from the set. - * - * @see HeaderWarning - */ -class ThrottlingLogger { - - // LRU set of keys used to determine if a message should be emitted to the logs - private final Set keys = Collections.newSetFromMap(Collections.synchronizedMap(new LinkedHashMap() { - @Override - protected boolean removeEldestEntry(final Map.Entry eldest) { - return size() > 128; - } - })); - - private final Logger logger; - - ThrottlingLogger(Logger logger) { - this.logger = logger; - } - - void throttleLog(String key, Message message) { - String xOpaqueId = HeaderWarning.getXOpaqueId(); - boolean shouldLog = keys.add(xOpaqueId + key); - if (shouldLog) { - log(message); - } - } - - private void log(Message message) { - AccessController.doPrivileged(new PrivilegedAction() { - @SuppressLoggerChecks(reason = "safely delegates to logger") - @Override - public Void run() { - logger.warn(message); - return null; - } - }); - } -} diff --git a/server/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java b/server/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java index 54e62bae4710f..8747f16d06651 100644 --- a/server/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java +++ b/server/src/test/java/org/elasticsearch/common/logging/DeprecationLoggerTests.java @@ -20,72 +20,11 @@ package org.elasticsearch.common.logging; import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.simple.SimpleLoggerContext; -import org.apache.logging.log4j.simple.SimpleLoggerContextFactory; -import org.apache.logging.log4j.spi.ExtendedLogger; -import org.apache.logging.log4j.spi.LoggerContext; -import org.apache.logging.log4j.spi.LoggerContextFactory; -import org.elasticsearch.common.SuppressLoggerChecks; import org.elasticsearch.test.ESTestCase; -import java.net.URI; -import java.security.AccessControlContext; -import java.security.AccessController; -import java.security.Permissions; -import java.security.PrivilegedAction; -import java.security.ProtectionDomain; -import java.util.concurrent.atomic.AtomicBoolean; - import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.core.Is.is; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; public class DeprecationLoggerTests extends ESTestCase { - @SuppressLoggerChecks(reason = "Safe as this is using mockito") - public void testLogPermissions() { - AtomicBoolean supplierCalled = new AtomicBoolean(false); - - // mocking the logger used inside DeprecationLogger requires heavy hacking... - ExtendedLogger mockLogger = mock(ExtendedLogger.class); - doAnswer(invocationOnMock -> { - supplierCalled.set(true); - createTempDir(); // trigger file permission, like rolling logs would - return null; - }).when(mockLogger).warn(DeprecatedMessage.of(any(), "foo")); - final LoggerContext context = new SimpleLoggerContext() { - @Override - public ExtendedLogger getLogger(String name) { - return mockLogger; - } - }; - - final LoggerContextFactory originalFactory = LogManager.getFactory(); - try { - LogManager.setFactory(new SimpleLoggerContextFactory() { - @Override - public LoggerContext getContext(String fqcn, ClassLoader loader, Object externalContext, boolean currentContext, - URI configLocation, String name) { - return context; - } - }); - DeprecationLogger deprecationLogger = DeprecationLogger.getLogger("logger"); - - AccessControlContext noPermissionsAcc = new AccessControlContext( - new ProtectionDomain[]{new ProtectionDomain(null, new Permissions())} - ); - AccessController.doPrivileged((PrivilegedAction) () -> { - deprecationLogger.deprecate("testLogPermissions_key", "foo {}", "bar"); - return null; - }, noPermissionsAcc); - assertThat("supplier called", supplierCalled.get(), is(true)); - - assertWarnings("foo bar"); - } finally { - LogManager.setFactory(originalFactory); - } - } public void testMultipleSlowLoggersUseSingleLog4jLogger() { org.apache.logging.log4j.core.LoggerContext context = (org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false); diff --git a/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java b/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java index 56cf86d2e7642..4dba05f4e4250 100644 --- a/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java +++ b/server/src/test/java/org/elasticsearch/common/logging/HeaderWarningTests.java @@ -48,8 +48,6 @@ public class HeaderWarningTests extends ESTestCase { private static final RegexMatcher warningValueMatcher = matches(WARNING_HEADER_PATTERN.pattern()); - private final HeaderWarning logger = new HeaderWarning(); - @Override protected boolean enableWarningsCheck() { //this is a low level test for the deprecation logger, setup and checks are done manually diff --git a/server/src/test/java/org/elasticsearch/common/logging/RateLimitingFilterTests.java b/server/src/test/java/org/elasticsearch/common/logging/RateLimitingFilterTests.java new file mode 100644 index 0000000000000..3f62ea7a715c0 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/common/logging/RateLimitingFilterTests.java @@ -0,0 +1,161 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.common.logging; + +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.message.SimpleMessage; +import org.elasticsearch.test.ESTestCase; +import org.junit.After; +import org.junit.Before; + +import static org.apache.logging.log4j.core.Filter.Result; +import static org.hamcrest.Matchers.equalTo; + +public class RateLimitingFilterTests extends ESTestCase { + + private RateLimitingFilter filter; + + @Before + public void setup() { + this.filter = new RateLimitingFilter(); + filter.start(); + } + + @After + public void cleanup() { + this.filter.stop(); + } + + /** + * Check that messages are rate-limited by their key. + */ + public void testMessagesAreRateLimitedByKey() { + // Fill up the cache + for (int i = 0; i < 128; i++) { + Message message = DeprecatedMessage.of("key " + i, "", "msg " + i); + assertThat("Expected key" + i + " to be accepted", filter.filter(message), equalTo(Result.ACCEPT)); + } + + // Should be rate-limited because it's still in the cache + Message message = DeprecatedMessage.of("key 0", "", "msg " + 0); + assertThat(filter.filter(message), equalTo(Result.DENY)); + + // Filter a message with a previously unseen key, in order to evict key0 as it's the oldest + message = DeprecatedMessage.of("key 129", "", "msg " + 129); + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + + // Should be allowed because key0 was evicted from the cache + message = DeprecatedMessage.of("key 0", "", "msg " + 0); + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + } + + /** + * Check that messages are rate-limited by their x-opaque-id value + */ + public void testMessagesAreRateLimitedByXOpaqueId() { + // Fill up the cache + for (int i = 0; i < 128; i++) { + Message message = DeprecatedMessage.of("", "id " + i, "msg " + i); + assertThat("Expected key" + i + " to be accepted", filter.filter(message), equalTo(Result.ACCEPT)); + } + + // Should be rate-limited because it's still in the cache + Message message = DeprecatedMessage.of("", "id 0", "msg 0"); + assertThat(filter.filter(message), equalTo(Result.DENY)); + + // Filter a message with a previously unseen key, in order to evict key0 as it's the oldest + message = DeprecatedMessage.of("", "id 129", "msg 129"); + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + + // Should be allowed because key0 was evicted from the cache + message = DeprecatedMessage.of("", "id 0", "msg 0"); + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + } + + /** + * Check that messages are rate-limited by their key and x-opaque-id value + */ + public void testMessagesAreRateLimitedByKeyAndXOpaqueId() { + // Fill up the cache + for (int i = 0; i < 128; i++) { + Message message = DeprecatedMessage.of("key " + i, "opaque-id " + i, "msg " + i); + assertThat("Expected key" + i + " to be accepted", filter.filter(message), equalTo(Result.ACCEPT)); + } + + // Should be rate-limited because it's still in the cache + Message message = DeprecatedMessage.of("key 0", "opaque-id 0", "msg 0"); + assertThat(filter.filter(message), equalTo(Result.DENY)); + + // Filter a message with a previously unseen key, in order to evict key0 as it's the oldest + message = DeprecatedMessage.of("key 129", "opaque-id 129", "msg 129"); + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + + // Should be allowed because key 0 was evicted from the cache + message = DeprecatedMessage.of("key 0", "opaque-id 0", "msg 0"); + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + } + + /** + * Check that it is the combination of key and x-opaque-id that rate-limits messages, by varying each + * independently and checking that a message is not filtered. + */ + public void testVariationsInKeyAndXOpaqueId() { + Message message = DeprecatedMessage.of("key 0", "opaque-id 0", "msg 0"); + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + + message = DeprecatedMessage.of("key 0", "opaque-id 0", "msg 0"); + // Rejected because the "x-opaque-id" and "key" values are the same as above + assertThat(filter.filter(message), equalTo(Result.DENY)); + + message = DeprecatedMessage.of("key 1", "opaque-id 0", "msg 0"); + // Accepted because the "key" value is different + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + + message = DeprecatedMessage.of("key 0", "opaque-id 1", "msg 0"); + // Accepted because the "x-opaque-id" value is different + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + } + + /** + * Check that rate-limiting is not applied to messages if they are not an EsLogMessage. + */ + public void testOnlyEsMessagesAreFiltered() { + Message message = new SimpleMessage("a message"); + assertThat(filter.filter(message), equalTo(Result.NEUTRAL)); + } + + /** + * Check that the filter can be reset, so that previously-seen keys are treated as new keys. + */ + public void testFilterCanBeReset() { + final Message message = DeprecatedMessage.of("key", "", "msg"); + + // First time, the message is a allowed + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + + // Second time, it is filtered out + assertThat(filter.filter(message), equalTo(Result.DENY)); + + filter.reset(); + + // Third time, it is allowed again + assertThat(filter.filter(message), equalTo(Result.ACCEPT)); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java index 487ed92d18077..b6afb999abc4f 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESTestCase.java @@ -65,6 +65,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.logging.HeaderWarning; +import org.elasticsearch.common.logging.HeaderWarningAppender; import org.elasticsearch.common.logging.LogConfigurator; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Setting; @@ -148,6 +149,7 @@ import static java.util.Collections.emptyMap; import static org.elasticsearch.common.util.CollectionUtils.arrayAsArrayList; +import static org.elasticsearch.tasks.Task.X_OPAQUE_ID; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -180,6 +182,7 @@ public abstract class ESTestCase extends LuceneTestCase { private static final AtomicInteger portGenerator = new AtomicInteger(); private static final Collection nettyLoggedLeaks = new ArrayList<>(); + private HeaderWarningAppender headerWarningAppender; @AfterClass public static void resetPortCounter() { @@ -335,6 +338,19 @@ public static void ensureSupportedLocale() { } } + @Before + public void setHeaderWarningAppender() { + this.headerWarningAppender = HeaderWarningAppender.createAppender("header_warning", null); + this.headerWarningAppender.start(); + Loggers.addAppender(LogManager.getLogger("org.elasticsearch.deprecation"), this.headerWarningAppender); + } + + @After + public void removeHeaderWarningAppender() { + Loggers.removeAppender(LogManager.getLogger("org.elasticsearch.deprecation"), this.headerWarningAppender); + this.headerWarningAppender = null; + } + @Before public final void before() { logger.info("{}before test", getTestParamsForLogging()); @@ -1456,4 +1472,7 @@ protected static InetAddress randomIp(boolean v4) { } } + public void setXOpaqueId(String value) { + threadContext.putHeader(X_OPAQUE_ID, value); + } } diff --git a/test/framework/src/main/resources/log4j2-test.properties b/test/framework/src/main/resources/log4j2-test.properties index 842c9c79d7963..88612e20dc7e2 100644 --- a/test/framework/src/main/resources/log4j2-test.properties +++ b/test/framework/src/main/resources/log4j2-test.properties @@ -5,3 +5,13 @@ appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] [%test_thread_i rootLogger.level = ${sys:tests.es.logger.level:-info} rootLogger.appenderRef.console.ref = console + +appender.header_warning.type = HeaderWarningAppender +appender.header_warning.name = header_warning + +logger.header_logger.name = header_logger +logger.header_logger.level = warn +logger.header_logger.appenderRef.header_warning.ref = header_warning + +logger.deprecation.name = org.elasticsearch.deprecation +logger.deprecation.level = deprecation diff --git a/x-pack/plugin/async-search/qa/rest/src/main/java/org/elasticsearch/query/DeprecatedQueryBuilder.java b/x-pack/plugin/async-search/qa/rest/src/main/java/org/elasticsearch/query/DeprecatedQueryBuilder.java index 0ce331e793d09..9cbf5f9bc97ad 100644 --- a/x-pack/plugin/async-search/qa/rest/src/main/java/org/elasticsearch/query/DeprecatedQueryBuilder.java +++ b/x-pack/plugin/async-search/qa/rest/src/main/java/org/elasticsearch/query/DeprecatedQueryBuilder.java @@ -21,7 +21,7 @@ import java.io.IOException; public class DeprecatedQueryBuilder extends AbstractQueryBuilder { - private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger("Deprecated"); + private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(DeprecatedQueryBuilder.class); public static final String NAME = "deprecated"; diff --git a/x-pack/plugin/deprecation/build.gradle b/x-pack/plugin/deprecation/build.gradle index 96a69520a76ca..8e5ceabd3db4f 100644 --- a/x-pack/plugin/deprecation/build.gradle +++ b/x-pack/plugin/deprecation/build.gradle @@ -1,5 +1,4 @@ apply plugin: 'elasticsearch.esplugin' -apply plugin: 'elasticsearch.internal-cluster-test' esplugin { name 'x-pack-deprecation' @@ -9,6 +8,15 @@ esplugin { } archivesBaseName = 'x-pack-deprecation' +// add all sub-projects of the qa sub-project +gradle.projectsEvaluated { + project.subprojects + .find { it.path == project.path + ":qa" } + .subprojects + .findAll { it.path.startsWith(project.path + ":qa") } + .each { check.dependsOn it.check } +} + dependencies { compileOnly project(":x-pack:plugin:core") } diff --git a/x-pack/plugin/deprecation/qa/build.gradle b/x-pack/plugin/deprecation/qa/build.gradle new file mode 100644 index 0000000000000..75d524cc11500 --- /dev/null +++ b/x-pack/plugin/deprecation/qa/build.gradle @@ -0,0 +1,8 @@ +import org.elasticsearch.gradle.test.RestIntegTestTask + +apply plugin: 'elasticsearch.build' +test.enabled = false + +dependencies { + api project(':test:framework') +} diff --git a/x-pack/plugin/deprecation/qa/rest/build.gradle b/x-pack/plugin/deprecation/qa/rest/build.gradle new file mode 100644 index 0000000000000..81581e2c9601e --- /dev/null +++ b/x-pack/plugin/deprecation/qa/rest/build.gradle @@ -0,0 +1,37 @@ +import org.elasticsearch.gradle.info.BuildParams + +apply plugin: 'elasticsearch.testclusters' +apply plugin: 'elasticsearch.esplugin' +apply plugin: 'elasticsearch.rest-resources' + +esplugin { + description 'Deprecated query plugin' + classname 'org.elasticsearch.xpack.deprecation.TestDeprecationPlugin' +} + +dependencies { + api("com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}") + api("com.fasterxml.jackson.core:jackson-databind:${versions.jackson}") +} + +restResources { + restApi { + includeCore '_common', 'indices', 'index' + } +} + +testClusters.integTest { + testDistribution = 'DEFAULT' + // add the deprecated query plugin + plugin file(project(':x-pack:plugin:deprecation:qa:rest').tasks.bundlePlugin.archiveFile) + setting 'xpack.security.enabled', 'false' +} + +test.enabled = false + +check.dependsOn integTest + +tasks.named("dependencyLicenses").configure { + mapping from: /jackson-.*/, to: 'jackson' +} + diff --git a/x-pack/plugin/deprecation/qa/rest/licenses/jackson-LICENSE b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-LICENSE new file mode 100644 index 0000000000000..f5f45d26a49d6 --- /dev/null +++ b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-LICENSE @@ -0,0 +1,8 @@ +This copy of Jackson JSON processor streaming parser/generator is licensed under the +Apache (Software) License, version 2.0 ("the License"). +See the License for details about distribution rights, and the +specific rights regarding derivate works. + +You may obtain a copy of the License at: + +http://www.apache.org/licenses/LICENSE-2.0 diff --git a/x-pack/plugin/deprecation/qa/rest/licenses/jackson-NOTICE b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-NOTICE new file mode 100644 index 0000000000000..4c976b7b4cc58 --- /dev/null +++ b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-NOTICE @@ -0,0 +1,20 @@ +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers, as well as supported +commercially by FasterXML.com. + +## Licensing + +Jackson core and extension components may licensed under different licenses. +To find the details that apply to this artifact see the accompanying LICENSE file. +For more information, including possible other licensing options, contact +FasterXML.com (http://fasterxml.com). + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. diff --git a/x-pack/plugin/deprecation/qa/rest/licenses/jackson-annotations-2.10.4.jar.sha1 b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-annotations-2.10.4.jar.sha1 new file mode 100644 index 0000000000000..0c548bb0e7711 --- /dev/null +++ b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-annotations-2.10.4.jar.sha1 @@ -0,0 +1 @@ +6ae6028aff033f194c9710ad87c224ccaadeed6c \ No newline at end of file diff --git a/x-pack/plugin/deprecation/qa/rest/licenses/jackson-databind-2.10.4.jar.sha1 b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-databind-2.10.4.jar.sha1 new file mode 100644 index 0000000000000..27d5a72cd27af --- /dev/null +++ b/x-pack/plugin/deprecation/qa/rest/licenses/jackson-databind-2.10.4.jar.sha1 @@ -0,0 +1 @@ +76e9152e93d4cf052f93a64596f633ba5b1c8ed9 \ No newline at end of file diff --git a/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecatedQueryBuilder.java b/x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecatedQueryBuilder.java similarity index 100% rename from x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecatedQueryBuilder.java rename to x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecatedQueryBuilder.java diff --git a/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecationHeaderRestAction.java b/x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecationHeaderRestAction.java similarity index 66% rename from x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecationHeaderRestAction.java rename to x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecationHeaderRestAction.java index fa3e3e205ceaa..f53c835252bcb 100644 --- a/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecationHeaderRestAction.java +++ b/x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecationHeaderRestAction.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,20 +34,35 @@ public class TestDeprecationHeaderRestAction extends BaseRestHandler { private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(TestDeprecationHeaderRestAction.class); - public static final Setting TEST_DEPRECATED_SETTING_TRUE1 = - Setting.boolSetting("test.setting.deprecated.true1", true, - Setting.Property.NodeScope, Setting.Property.Deprecated, Setting.Property.Dynamic); - public static final Setting TEST_DEPRECATED_SETTING_TRUE2 = - Setting.boolSetting("test.setting.deprecated.true2", true, - Setting.Property.NodeScope, Setting.Property.Deprecated, Setting.Property.Dynamic); - public static final Setting TEST_NOT_DEPRECATED_SETTING = - Setting.boolSetting("test.setting.not_deprecated", false, - Setting.Property.NodeScope, Setting.Property.Dynamic); + public static final Setting TEST_DEPRECATED_SETTING_TRUE1 = Setting.boolSetting( + "test.setting.deprecated.true1", + true, + Setting.Property.NodeScope, + Setting.Property.Deprecated, + Setting.Property.Dynamic + ); + public static final Setting TEST_DEPRECATED_SETTING_TRUE2 = Setting.boolSetting( + "test.setting.deprecated.true2", + true, + Setting.Property.NodeScope, + Setting.Property.Deprecated, + Setting.Property.Dynamic + ); + public static final Setting TEST_NOT_DEPRECATED_SETTING = Setting.boolSetting( + "test.setting.not_deprecated", + false, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); private static final Map> SETTINGS_MAP = Map.of( - TEST_DEPRECATED_SETTING_TRUE1.getKey(), TEST_DEPRECATED_SETTING_TRUE1, - TEST_DEPRECATED_SETTING_TRUE2.getKey(), TEST_DEPRECATED_SETTING_TRUE2, - TEST_NOT_DEPRECATED_SETTING.getKey(), TEST_NOT_DEPRECATED_SETTING); + TEST_DEPRECATED_SETTING_TRUE1.getKey(), + TEST_DEPRECATED_SETTING_TRUE1, + TEST_DEPRECATED_SETTING_TRUE2.getKey(), + TEST_DEPRECATED_SETTING_TRUE2, + TEST_NOT_DEPRECATED_SETTING.getKey(), + TEST_NOT_DEPRECATED_SETTING + ); public static final String DEPRECATED_ENDPOINT = "[/_test_cluster/deprecated_settings] exists for deprecated tests"; public static final String DEPRECATED_USAGE = "[deprecated_settings] usage is deprecated. use [settings] instead"; @@ -59,8 +75,7 @@ public TestDeprecationHeaderRestAction(Settings settings) { @Override public List deprecatedRoutes() { - return singletonList( - new DeprecatedRoute(GET, "/_test_cluster/deprecated_settings", DEPRECATED_ENDPOINT)); + return singletonList(new DeprecatedRoute(GET, "/_test_cluster/deprecated_settings", DEPRECATED_ENDPOINT)); } @Override @@ -84,18 +99,25 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client if (source.containsKey("deprecated_settings")) { deprecationLogger.deprecate("deprecated_settings", DEPRECATED_USAGE); - settings = (List)source.get("deprecated_settings"); + settings = (List) source.get("deprecated_settings"); } else { - settings = (List)source.get("settings"); + settings = (List) source.get("settings"); } } + // Pull out the settings values here in order to guarantee that any deprecation messages are triggered + // in the same thread context. + final Map settingsMap = new HashMap<>(); + for (String setting : settings) { + settingsMap.put(setting, SETTINGS_MAP.get(setting).get(this.settings)); + } + return channel -> { final XContentBuilder builder = channel.newBuilder(); builder.startObject().startArray("settings"); - for (String setting : settings) { - builder.startObject().field(setting, SETTINGS_MAP.get(setting).get(this.settings)).endObject(); + for (Map.Entry entry : settingsMap.entrySet()) { + builder.startObject().field(entry.getKey(), entry.getValue()).endObject(); } builder.endArray().endObject(); channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder)); diff --git a/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecationPlugin.java b/x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecationPlugin.java similarity index 75% rename from x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecationPlugin.java rename to x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecationPlugin.java index e14f32f35680c..8297e9a5627b5 100644 --- a/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/TestDeprecationPlugin.java +++ b/x-pack/plugin/deprecation/qa/rest/src/main/java/org/elasticsearch/xpack/deprecation/TestDeprecationPlugin.java @@ -31,9 +31,15 @@ public class TestDeprecationPlugin extends Plugin implements ActionPlugin, SearchPlugin { @Override - public List getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings, - IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter, IndexNameExpressionResolver indexNameExpressionResolver, - Supplier nodesInCluster) { + public List getRestHandlers( + Settings settings, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster + ) { return Collections.singletonList(new TestDeprecationHeaderRestAction(settings)); } @@ -42,13 +48,15 @@ public List> getSettings() { return Arrays.asList( TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1, TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE2, - TestDeprecationHeaderRestAction.TEST_NOT_DEPRECATED_SETTING); + TestDeprecationHeaderRestAction.TEST_NOT_DEPRECATED_SETTING + ); } @Override public List> getQueries() { - return singletonList(new QuerySpec<>(TestDeprecatedQueryBuilder.NAME, TestDeprecatedQueryBuilder::new, - TestDeprecatedQueryBuilder::fromXContent)); + return singletonList( + new QuerySpec<>(TestDeprecatedQueryBuilder.NAME, TestDeprecatedQueryBuilder::new, TestDeprecatedQueryBuilder::fromXContent) + ); } } diff --git a/x-pack/plugin/deprecation/qa/rest/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java b/x-pack/plugin/deprecation/qa/rest/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java new file mode 100644 index 0000000000000..36e21eb1554d5 --- /dev/null +++ b/x-pack/plugin/deprecation/qa/rest/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java @@ -0,0 +1,312 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.deprecation; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.HeaderWarning; +import org.elasticsearch.common.logging.LoggerMessageFormat; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.test.rest.ESRestTestCase; +import org.hamcrest.Matcher; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; +import static org.elasticsearch.xpack.deprecation.TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1; +import static org.elasticsearch.xpack.deprecation.TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE2; +import static org.elasticsearch.xpack.deprecation.TestDeprecationHeaderRestAction.TEST_NOT_DEPRECATED_SETTING; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; + +/** + * Tests {@code DeprecationLogger} uses the {@code ThreadContext} to add response headers. + */ +public class DeprecationHttpIT extends ESRestTestCase { + + /** + * Check that configuring deprecation settings causes a warning to be added to the + * response headers. + */ + public void testDeprecatedSettingsReturnWarnings() throws IOException { + XContentBuilder builder = JsonXContent.contentBuilder() + .startObject() + .startObject("transient") + .field(TEST_DEPRECATED_SETTING_TRUE1.getKey(), !TEST_DEPRECATED_SETTING_TRUE1.getDefault(Settings.EMPTY)) + .field(TEST_DEPRECATED_SETTING_TRUE2.getKey(), !TEST_DEPRECATED_SETTING_TRUE2.getDefault(Settings.EMPTY)) + // There should be no warning for this field + .field(TEST_NOT_DEPRECATED_SETTING.getKey(), !TEST_NOT_DEPRECATED_SETTING.getDefault(Settings.EMPTY)) + .endObject() + .endObject(); + + final Request request = new Request("PUT", "_cluster/settings"); + request.setJsonEntity(Strings.toString(builder)); + final Response response = client().performRequest(request); + + final List deprecatedWarnings = getWarningHeaders(response.getHeaders()); + final List> headerMatchers = new ArrayList<>(2); + + for (Setting setting : List.of(TEST_DEPRECATED_SETTING_TRUE1, TEST_DEPRECATED_SETTING_TRUE2)) { + headerMatchers.add( + equalTo( + "[" + + setting.getKey() + + "] setting was deprecated in Elasticsearch and will be removed in a future release! " + + "See the breaking changes documentation for the next major version." + ) + ); + } + + assertThat(deprecatedWarnings, hasSize(headerMatchers.size())); + for (final String deprecatedWarning : deprecatedWarnings) { + assertThat( + "Header does not conform to expected pattern", + deprecatedWarning, + matches(HeaderWarning.WARNING_HEADER_PATTERN.pattern()) + ); + } + + final List actualWarningValues = deprecatedWarnings.stream() + .map(s -> HeaderWarning.extractWarningValueFromWarningHeader(s, true)) + .collect(Collectors.toList()); + for (Matcher headerMatcher : headerMatchers) { + assertThat(actualWarningValues, hasItem(headerMatcher)); + } + } + + /** + * Attempts to do a scatter/gather request that expects unique responses per sub-request. + */ + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/19222") + public void testUniqueDeprecationResponsesMergedTogether() throws IOException { + final String[] indices = new String[randomIntBetween(2, 5)]; + + // add at least one document for each index + for (int i = 0; i < indices.length; ++i) { + indices[i] = "test" + i; + + // create indices with a single shard to reduce noise; the query only deprecates uniquely by index anyway + createIndex(indices[i], Settings.builder().put("number_of_shards", 1).build()); + + int randomDocCount = randomIntBetween(1, 2); + + for (int j = 0; j < randomDocCount; j++) { + final Request request = new Request("PUT", indices[i] + "/" + j); + request.setJsonEntity("{ \"field\": " + j + " }"); + assertOK(client().performRequest(request)); + } + } + + final String commaSeparatedIndices = String.join(",", indices); + + client().performRequest(new Request("POST", commaSeparatedIndices + "/_refresh")); + + // trigger all index deprecations + Request request = new Request("GET", "/" + commaSeparatedIndices + "/_search"); + request.setJsonEntity("{ \"query\": { \"bool\": { \"filter\": [ { \"deprecated\": {} } ] } } }"); + Response response = client().performRequest(request); + assertOK(response); + + final List deprecatedWarnings = getWarningHeaders(response.getHeaders()); + final List> headerMatchers = new ArrayList<>(); + + for (String index : indices) { + headerMatchers.add(containsString(LoggerMessageFormat.format("[{}] index", (Object) index))); + } + + assertThat(deprecatedWarnings, hasSize(headerMatchers.size())); + for (Matcher headerMatcher : headerMatchers) { + assertThat(deprecatedWarnings, hasItem(headerMatcher)); + } + } + + public void testDeprecationWarningsAppearInHeaders() throws Exception { + doTestDeprecationWarningsAppearInHeaders(); + } + + public void testDeprecationHeadersDoNotGetStuck() throws Exception { + doTestDeprecationWarningsAppearInHeaders(); + doTestDeprecationWarningsAppearInHeaders(); + if (rarely()) { + doTestDeprecationWarningsAppearInHeaders(); + } + } + + /** + * Run a request that receives a predictably randomized number of deprecation warnings. + *

+ * Re-running this back-to-back helps to ensure that warnings are not being maintained across requests. + */ + private void doTestDeprecationWarningsAppearInHeaders() throws IOException { + final boolean useDeprecatedField = randomBoolean(); + final boolean useNonDeprecatedSetting = randomBoolean(); + + // deprecated settings should also trigger a deprecation warning + final List> settings = new ArrayList<>(3); + settings.add(TEST_DEPRECATED_SETTING_TRUE1); + + if (randomBoolean()) { + settings.add(TEST_DEPRECATED_SETTING_TRUE2); + } + + if (useNonDeprecatedSetting) { + settings.add(TEST_NOT_DEPRECATED_SETTING); + } + + Collections.shuffle(settings, random()); + + // trigger all deprecations + Request request = new Request("GET", "/_test_cluster/deprecated_settings"); + request.setEntity(buildSettingsRequest(settings, useDeprecatedField)); + Response response = client().performRequest(request); + assertOK(response); + + final List deprecatedWarnings = getWarningHeaders(response.getHeaders()); + final List> headerMatchers = new ArrayList<>(4); + + headerMatchers.add(equalTo(TestDeprecationHeaderRestAction.DEPRECATED_ENDPOINT)); + if (useDeprecatedField) { + headerMatchers.add(equalTo(TestDeprecationHeaderRestAction.DEPRECATED_USAGE)); + } + + assertThat(deprecatedWarnings, hasSize(headerMatchers.size())); + for (final String deprecatedWarning : deprecatedWarnings) { + assertThat(deprecatedWarning, matches(HeaderWarning.WARNING_HEADER_PATTERN.pattern())); + } + final List actualWarningValues = deprecatedWarnings.stream() + .map(s -> HeaderWarning.extractWarningValueFromWarningHeader(s, true)) + .collect(Collectors.toList()); + for (Matcher headerMatcher : headerMatchers) { + assertThat(actualWarningValues, hasItem(headerMatcher)); + } + } + + /** + * Check that deprecation messages can be recorded to an index + */ + public void testDeprecationMessagesCanBeIndexed() throws Exception { + try { + configureWriteDeprecationLogsToIndex(true); + + Request request = new Request("GET", "/_test_cluster/deprecated_settings"); + request.setEntity(buildSettingsRequest(List.of(TEST_DEPRECATED_SETTING_TRUE1), true)); + assertOK(client().performRequest(request)); + + assertBusy(() -> { + Response response; + try { + response = client().performRequest(new Request("GET", "logs-deprecation-elasticsearch/_search")); + } catch (Exception e) { + // It can take a moment for the index to be created. If it doesn't exist then the client + // throws an exception. Translate it into an assertion error so that assertBusy() will + // continue trying. + throw new AssertionError(e); + } + assertOK(response); + + ObjectMapper mapper = new ObjectMapper(); + final JsonNode jsonNode = mapper.readTree(response.getEntity().getContent()); + + final int hits = jsonNode.at("/hits/total/value").intValue(); + assertThat(hits, greaterThan(0)); + + List> documents = new ArrayList<>(); + + for (int i = 0; i < hits; i++) { + final JsonNode hit = jsonNode.at("/hits/hits/" + i + "/_source"); + + final Map document = new HashMap<>(); + hit.fields().forEachRemaining(entry -> document.put(entry.getKey(), entry.getValue().textValue())); + + documents.add(document); + } + + logger.warn(documents); + assertThat(documents, hasSize(2)); + + assertThat( + documents, + hasItems( + hasEntry("message", "[deprecated_settings] usage is deprecated. use [settings] instead"), + hasEntry("message", "[/_test_cluster/deprecated_settings] exists for deprecated tests") + ) + ); + }); + } finally { + configureWriteDeprecationLogsToIndex(null); + client().performRequest(new Request("DELETE", "_data_stream/logs-deprecation-elasticsearch")); + } + } + + private void configureWriteDeprecationLogsToIndex(Boolean value) throws IOException { + final Request request = new Request("PUT", "_cluster/settings"); + request.setJsonEntity("{ \"transient\": { \"cluster.deprecation_indexing.enabled\": " + value + " } }"); + final Response response = client().performRequest(request); + assertOK(response); + } + + private List getWarningHeaders(Header[] headers) { + List warnings = new ArrayList<>(); + + for (Header header : headers) { + if (header.getName().equals("Warning")) { + warnings.add(header.getValue()); + } + } + + return warnings; + } + + private HttpEntity buildSettingsRequest(List> settings, boolean useDeprecatedField) throws IOException { + XContentBuilder builder = JsonXContent.contentBuilder(); + + builder.startObject().startArray(useDeprecatedField ? "deprecated_settings" : "settings"); + + for (Setting setting : settings) { + builder.value(setting.getKey()); + } + + builder.endArray().endObject(); + + return new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); + } + + /** + * Builds a REST client that will tolerate warnings in the response headers. The default + * is to throw an exception. + */ + @Override + protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOException { + RestClientBuilder builder = RestClient.builder(hosts); + configureClient(builder, settings); + builder.setStrictDeprecationMode(false); + return builder.build(); + } +} diff --git a/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java b/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java deleted file mode 100644 index 8040366134c1c..0000000000000 --- a/x-pack/plugin/deprecation/src/internalClusterTest/java/org/elasticsearch/xpack/deprecation/DeprecationHttpIT.java +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.xpack.deprecation; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpHost; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; -import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; -import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; -import org.elasticsearch.client.Client; -import org.elasticsearch.client.Request; -import org.elasticsearch.client.Response; -import org.elasticsearch.client.RestClient; -import org.elasticsearch.client.RestClientBuilder; -import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.HeaderWarning; -import org.elasticsearch.common.logging.LoggerMessageFormat; -import org.elasticsearch.common.network.NetworkAddress; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.transport.TransportAddress; -import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentType; -import org.elasticsearch.common.xcontent.json.JsonXContent; -import org.elasticsearch.core.internal.io.IOUtils; -import org.elasticsearch.http.HttpInfo; -import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.test.ESSingleNodeTestCase; -import org.elasticsearch.transport.Netty4Plugin; -import org.elasticsearch.xpack.core.XPackPlugin; -import org.hamcrest.Matcher; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import static org.elasticsearch.rest.RestStatus.OK; -import static org.elasticsearch.test.hamcrest.RegexMatcher.matches; -import static org.elasticsearch.xpack.deprecation.TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE1; -import static org.elasticsearch.xpack.deprecation.TestDeprecationHeaderRestAction.TEST_DEPRECATED_SETTING_TRUE2; -import static org.elasticsearch.xpack.deprecation.TestDeprecationHeaderRestAction.TEST_NOT_DEPRECATED_SETTING; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.hasSize; - -/** - * Tests {@code DeprecationLogger} uses the {@code ThreadContext} to add response headers. - */ -public class DeprecationHttpIT extends ESSingleNodeTestCase { - - private static RestClient restClient; - - @Override - protected boolean addMockHttpTransport() { - return false; // enable http - } - - @Override - protected Collection> getPlugins() { - return List.of(Netty4Plugin.class, XPackPlugin.class, Deprecation.class, TestDeprecationPlugin.class); - } - - @Override - protected Settings nodeSettings() { - return Settings.builder() - // change values of deprecated settings so that accessing them is logged - .put(TEST_DEPRECATED_SETTING_TRUE1.getKey(), ! TEST_DEPRECATED_SETTING_TRUE1.getDefault(Settings.EMPTY)) - .put(TEST_DEPRECATED_SETTING_TRUE2.getKey(), ! TEST_DEPRECATED_SETTING_TRUE2.getDefault(Settings.EMPTY)) - // non-deprecated setting to ensure not everything is logged - .put(TEST_NOT_DEPRECATED_SETTING.getKey(), ! TEST_NOT_DEPRECATED_SETTING.getDefault(Settings.EMPTY)) - .build(); - } - - /** - * Attempts to do a scatter/gather request that expects unique responses per sub-request. - */ - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/19222") - public void testUniqueDeprecationResponsesMergedTogether() throws IOException { - final String[] indices = new String[randomIntBetween(2, 5)]; - - // add at least one document for each index - for (int i = 0; i < indices.length; ++i) { - indices[i] = "test" + i; - - // create indices with a single shard to reduce noise; the query only deprecates uniquely by index anyway - assertTrue( - client().admin() - .indices() - .prepareCreate(indices[i]) - .setSettings(Settings.builder().put("number_of_shards", 1)) - .get() - .isAcknowledged() - ); - - int randomDocCount = randomIntBetween(1, 2); - - for (int j = 0; j < randomDocCount; ++j) { - client().prepareIndex(indices[i]) - .setId(Integer.toString(j)) - .setSource("{\"field\":" + j + "}", XContentType.JSON) - .execute() - .actionGet(); - } - } - - client().admin().indices().refresh(new RefreshRequest(indices)); - - final String commaSeparatedIndices = String.join(",", indices); - - // trigger all index deprecations - Request request = new Request("GET", "/" + commaSeparatedIndices + "/_search"); - request.setJsonEntity("{\"query\":{\"bool\":{\"filter\":[{\"" + TestDeprecatedQueryBuilder.NAME + "\":{}}]}}}"); - Response response = getRestClient().performRequest(request); - assertThat(response.getStatusLine().getStatusCode(), equalTo(OK.getStatus())); - - final List deprecatedWarnings = getWarningHeaders(response.getHeaders()); - final List> headerMatchers = new ArrayList<>(indices.length); - - for (String index : indices) { - headerMatchers.add(containsString(LoggerMessageFormat.format("[{}] index", (Object)index))); - } - - assertThat(deprecatedWarnings, hasSize(headerMatchers.size())); - for (Matcher headerMatcher : headerMatchers) { - assertThat(deprecatedWarnings, hasItem(headerMatcher)); - } - } - - public void testDeprecationWarningsAppearInHeaders() throws Exception { - doTestDeprecationWarningsAppearInHeaders(); - } - - public void testDeprecationHeadersDoNotGetStuck() throws Exception { - doTestDeprecationWarningsAppearInHeaders(); - doTestDeprecationWarningsAppearInHeaders(); - if (rarely()) { - doTestDeprecationWarningsAppearInHeaders(); - } - } - - /** - * Run a request that receives a predictably randomized number of deprecation warnings. - *

- * Re-running this back-to-back helps to ensure that warnings are not being maintained across requests. - */ - private void doTestDeprecationWarningsAppearInHeaders() throws IOException { - final boolean useDeprecatedField = randomBoolean(); - final boolean useNonDeprecatedSetting = randomBoolean(); - - // deprecated settings should also trigger a deprecation warning - final List> settings = new ArrayList<>(3); - settings.add(TEST_DEPRECATED_SETTING_TRUE1); - - if (randomBoolean()) { - settings.add(TEST_DEPRECATED_SETTING_TRUE2); - } - - if (useNonDeprecatedSetting) { - settings.add(TEST_NOT_DEPRECATED_SETTING); - } - - Collections.shuffle(settings, random()); - - // trigger all deprecations - Request request = new Request("GET", "/_test_cluster/deprecated_settings"); - request.setEntity(buildSettingsRequest(settings, useDeprecatedField)); - Response response = getRestClient().performRequest(request); - assertThat(response.getStatusLine().getStatusCode(), equalTo(OK.getStatus())); - - final List deprecatedWarnings = getWarningHeaders(response.getHeaders()); - final List> headerMatchers = new ArrayList<>(4); - - headerMatchers.add(equalTo(TestDeprecationHeaderRestAction.DEPRECATED_ENDPOINT)); - if (useDeprecatedField) { - headerMatchers.add(equalTo(TestDeprecationHeaderRestAction.DEPRECATED_USAGE)); - } - for (Setting setting : settings) { - if (setting.isDeprecated()) { - headerMatchers.add(equalTo( - "[" + setting.getKey() + "] setting was deprecated in Elasticsearch and will be removed in a future release! " + - "See the breaking changes documentation for the next major version.")); - } - } - - assertThat(deprecatedWarnings, hasSize(headerMatchers.size())); - for (final String deprecatedWarning : deprecatedWarnings) { - assertThat(deprecatedWarning, matches(HeaderWarning.WARNING_HEADER_PATTERN.pattern())); - } - final List actualWarningValues = - deprecatedWarnings.stream().map(s -> HeaderWarning.extractWarningValueFromWarningHeader(s, true)) - .collect(Collectors.toList()); - for (Matcher headerMatcher : headerMatchers) { - assertThat(actualWarningValues, hasItem(headerMatcher)); - } - } - - private List getWarningHeaders(Header[] headers) { - List warnings = new ArrayList<>(); - - for (Header header : headers) { - if (header.getName().equals("Warning")) { - warnings.add(header.getValue()); - } - } - - return warnings; - } - - private HttpEntity buildSettingsRequest(List> settings, boolean useDeprecatedField) throws IOException { - XContentBuilder builder = JsonXContent.contentBuilder(); - - builder.startObject().startArray(useDeprecatedField ? "deprecated_settings" : "settings"); - - for (Setting setting : settings) { - builder.value(setting.getKey()); - } - - builder.endArray().endObject(); - - return new StringEntity(Strings.toString(builder), ContentType.APPLICATION_JSON); - } - - protected RestClient getRestClient() { - return getRestClient(client()); - } - - private static synchronized RestClient getRestClient(Client client) { - if (restClient == null) { - restClient = buildRestClient(client); - } - return restClient; - } - - private static RestClient buildRestClient(Client client ) { - NodesInfoResponse nodesInfoResponse = client.admin().cluster().prepareNodesInfo().get(); - assertFalse(nodesInfoResponse.hasFailures()); - assertThat(nodesInfoResponse.getNodes(), hasSize(1)); - - NodeInfo node = nodesInfoResponse.getNodes().get(0); - assertNotNull(node.getInfo(HttpInfo.class)); - - TransportAddress publishAddress = node.getInfo(HttpInfo.class).address().publishAddress(); - InetSocketAddress address = publishAddress.address(); - final HttpHost host = new HttpHost(NetworkAddress.format(address.getAddress()), address.getPort(), "http"); - RestClientBuilder builder = RestClient.builder(host); - return builder.build(); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - if (restClient != null) { - IOUtils.closeWhileHandlingException(restClient); - restClient = null; - } - } -} diff --git a/x-pack/plugin/deprecation/src/internalClusterTest/resources/plugin-security.policy b/x-pack/plugin/deprecation/src/internalClusterTest/resources/plugin-security.policy deleted file mode 100644 index a11e2427783af..0000000000000 --- a/x-pack/plugin/deprecation/src/internalClusterTest/resources/plugin-security.policy +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -grant { - permission java.lang.RuntimePermission "*", "setContextClassLoader"; -}; - diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java index 6d17f634c9a78..2415c71fddf02 100644 --- a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/Deprecation.java @@ -5,26 +5,40 @@ */ package org.elasticsearch.xpack.deprecation; - import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.client.Client; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.env.Environment; +import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.plugins.ActionPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.repositories.RepositoriesService; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.deprecation.DeprecationInfoAction; import org.elasticsearch.xpack.core.deprecation.NodesDeprecationCheckAction; +import org.elasticsearch.xpack.deprecation.logging.DeprecationIndexingComponent; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.Supplier; +import static org.elasticsearch.xpack.deprecation.logging.DeprecationIndexingComponent.WRITE_DEPRECATION_LOGS_TO_INDEX; + /** * The plugin class for the Deprecation API */ @@ -46,4 +60,30 @@ public List getRestHandlers(Settings settings, RestController restC return Collections.singletonList(new RestDeprecationInfoAction()); } + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { + DeprecationIndexingComponent component = new DeprecationIndexingComponent(threadPool, client); + + clusterService.addListener(component); + + return List.of(component); + } + + @Override + public List> getSettings() { + return List.of(WRITE_DEPRECATION_LOGS_TO_INDEX); + } } diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingAppender.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingAppender.java new file mode 100644 index 0000000000000..33ee4e29318fc --- /dev/null +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingAppender.java @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.deprecation.logging; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.elasticsearch.action.DocWriteRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.common.xcontent.XContentType; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * This log4j appender writes deprecation log messages to an index. It does not perform the actual + * writes, but instead constructs an {@link IndexRequest} for the log message and passes that + * to a callback. + */ +@Plugin(name = "DeprecationIndexingAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) +public class DeprecationIndexingAppender extends AbstractAppender { + public static final String DEPRECATION_MESSAGES_DATA_STREAM = "logs-deprecation-elasticsearch"; + + private final Consumer requestConsumer; + + /** + * You can't start and stop an appender to toggle it, so this flag reflects whether + * writes should in fact be carried out. + */ + private volatile boolean isEnabled = false; + + /** + * Creates a new appender. + * @param name the appender's name + * @param filter a filter to apply directly on the appender + * @param layout the layout to use for formatting message. It must return a JSON string. + * @param requestConsumer a callback to handle the actual indexing of the log message. + */ + public DeprecationIndexingAppender(String name, Filter filter, Layout layout, Consumer requestConsumer) { + super(name, filter, layout); + this.requestConsumer = Objects.requireNonNull(requestConsumer, "requestConsumer cannot be null"); + } + + /** + * Constructs an index request for a deprecation message, and passes it to the callback that was + * supplied to {@link #DeprecationIndexingAppender(String, Filter, Layout, Consumer)}. + */ + @Override + public void append(LogEvent event) { + if (this.isEnabled == false) { + return; + } + + final byte[] payload = this.getLayout().toByteArray(event); + + final IndexRequest request = new IndexRequest(DEPRECATION_MESSAGES_DATA_STREAM).source(payload, XContentType.JSON) + .opType(DocWriteRequest.OpType.CREATE); + + this.requestConsumer.accept(request); + } + + /** + * Sets whether this appender is enabled or disabled. When disabled, the appender will + * not perform indexing operations. + * @param isEnabled the enabled status of the appender. + */ + public void setEnabled(boolean isEnabled) { + this.isEnabled = isEnabled; + } + + /** + * Returns whether the appender is enabled i.e. performing indexing operations. + */ + public boolean isEnabled() { + return isEnabled; + } +} diff --git a/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingComponent.java b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingComponent.java new file mode 100644 index 0000000000000..376f3f3fb86ba --- /dev/null +++ b/x-pack/plugin/deprecation/src/main/java/org/elasticsearch/xpack/deprecation/logging/DeprecationIndexingComponent.java @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.deprecation.logging; + +import co.elastic.logging.log4j2.EcsLayout; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.elasticsearch.action.bulk.BulkProcessor; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.OriginSettingClient; +import org.elasticsearch.cluster.ClusterChangedEvent; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.ClusterStateListener; +import org.elasticsearch.common.component.AbstractLifecycleComponent; +import org.elasticsearch.common.logging.ECSJsonLayout; +import org.elasticsearch.common.logging.Loggers; +import org.elasticsearch.common.logging.RateLimitingFilter; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.ClientHelper; + +import java.util.function.Consumer; + +/** + * This component manages the construction and lifecycle of the {@link DeprecationIndexingAppender}. + * It also starts and stops the appender + */ +public class DeprecationIndexingComponent extends AbstractLifecycleComponent implements ClusterStateListener { + private static final Logger logger = LogManager.getLogger(DeprecationIndexingComponent.class); + + public static final Setting WRITE_DEPRECATION_LOGS_TO_INDEX = Setting.boolSetting( + "cluster.deprecation_indexing.enabled", + false, + Setting.Property.NodeScope, + Setting.Property.Dynamic + ); + + private final DeprecationIndexingAppender appender; + private final BulkProcessor processor; + private final RateLimitingFilter filter; + + public DeprecationIndexingComponent(ThreadPool threadPool, Client client) { + this.processor = getBulkProcessor(new OriginSettingClient(client, ClientHelper.DEPRECATION_ORIGIN)); + final Consumer consumer = buildIndexRequestConsumer(threadPool); + + final LoggerContext context = (LoggerContext) LogManager.getContext(false); + final Configuration configuration = context.getConfiguration(); + + final EcsLayout ecsLayout = ECSJsonLayout.newBuilder().setType("deprecation").setConfiguration(configuration).build(); + + this.filter = new RateLimitingFilter(); + this.appender = new DeprecationIndexingAppender("deprecation_indexing_appender", filter, ecsLayout, consumer); + } + + @Override + protected void doStart() { + this.appender.start(); + Loggers.addAppender(LogManager.getLogger("org.elasticsearch.deprecation"), this.appender); + } + + @Override + protected void doStop() { + Loggers.removeAppender(LogManager.getLogger("org.elasticsearch.deprecation"), this.appender); + this.appender.stop(); + } + + @Override + protected void doClose() { + this.processor.close(); + } + + /** + * Listens for changes to the cluster state, in order to know whether to toggle indexing + * and to set the cluster UUID and node ID. These can't be set in the constructor because + * the initial cluster state won't be set yet. + * + * @param event the cluster state event to process + */ + @Override + public void clusterChanged(ClusterChangedEvent event) { + final ClusterState state = event.state(); + final boolean newEnabled = WRITE_DEPRECATION_LOGS_TO_INDEX.get(state.getMetadata().settings()); + if (appender.isEnabled() != newEnabled) { + // We've flipped from disabled to enabled. Make sure we start with a clean cache of + // previously-seen keys, otherwise we won't index anything. + if (newEnabled) { + this.filter.reset(); + } + appender.setEnabled(newEnabled); + } + } + + /** + * Constructs a {@link Consumer} that knows what to do with the {@link IndexRequest} instances that the + * {@link DeprecationIndexingAppender} creates. This logic is separated from the service in order to make + * testing significantly easier, and to separate concerns. + *

+ * Writes are done via {@link BulkProcessor}, which handles batching up writes and retries. + * + * @param threadPool due to #50440, + * extra care must be taken to avoid blocking the thread that writes a deprecation message. + * @return a consumer that accepts an index request and handles all the details of writing it + * into the cluster + */ + private Consumer buildIndexRequestConsumer(ThreadPool threadPool) { + return indexRequest -> { + try { + // TODO: remove the threadpool wrapping when the .add call is non-blocking + // (it can currently execute the bulk request occasionally) + // see: https://github.com/elastic/elasticsearch/issues/50440 + threadPool.executor(ThreadPool.Names.GENERIC).execute(() -> this.processor.add(indexRequest)); + } catch (Exception e) { + logger.error("Failed to queue deprecation message index request: " + e.getMessage(), e); + } + }; + } + + /** + * Constructs a bulk processor for writing documents + * @param client the client to use + * @return an initialised bulk processor + */ + private BulkProcessor getBulkProcessor(Client client) { + final OriginSettingClient originSettingClient = new OriginSettingClient(client, ClientHelper.DEPRECATION_ORIGIN); + final BulkProcessor.Listener listener = new DeprecationBulkListener(); + + return BulkProcessor.builder(originSettingClient::bulk, listener) + .setBulkActions(100) + .setFlushInterval(TimeValue.timeValueSeconds(5)) + .build(); + } + + private static class DeprecationBulkListener implements BulkProcessor.Listener { + @Override + public void beforeBulk(long executionId, BulkRequest request) {} + + @Override + public void afterBulk(long executionId, BulkRequest request, BulkResponse response) {} + + @Override + public void afterBulk(long executionId, BulkRequest request, Throwable failure) { + logger.error("Bulk write of deprecation logs failed: " + failure.getMessage(), failure); + } + } +} diff --git a/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationIndexingAppenderTests.java b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationIndexingAppenderTests.java new file mode 100644 index 0000000000000..5475efcf4c7e4 --- /dev/null +++ b/x-pack/plugin/deprecation/src/test/java/org/elasticsearch/xpack/deprecation/DeprecationIndexingAppenderTests.java @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.deprecation; + +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.deprecation.logging.DeprecationIndexingAppender; +import org.junit.Before; +import org.mockito.ArgumentCaptor; + +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.hasEntry; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DeprecationIndexingAppenderTests extends ESTestCase { + + private DeprecationIndexingAppender appender; + private Layout layout; + private Consumer consumer; + + @Before + @SuppressWarnings("unchecked") + public void initialize() { + layout = mock(Layout.class); + consumer = mock(Consumer.class); + appender = new DeprecationIndexingAppender("a name", null, layout, consumer); + } + + /** + * Checks that the service does not attempt to index messages when the service + * is disabled. + */ + public void testDoesNotWriteMessageWhenServiceDisabled() { + appender.append(mock(LogEvent.class)); + + verify(consumer, never()).accept(any()); + } + + /** + * Checks that the service can be disabled after being enabled. + */ + public void testDoesNotWriteMessageWhenServiceEnabledAndDisabled() { + appender.setEnabled(true); + appender.setEnabled(false); + + appender.append(mock(LogEvent.class)); + + verify(consumer, never()).accept(any()); + } + + /** + * Checks that messages are indexed in the correct shape when the service is enabled. + * Formatted is handled entirely by the configured Layout, so that is not verified here. + */ + public void testWritesMessageWhenServiceEnabled() { + appender.setEnabled(true); + + when(layout.toByteArray(any())).thenReturn("{ \"some key\": \"some value\" }".getBytes(StandardCharsets.UTF_8)); + + appender.append(mock(LogEvent.class)); + + ArgumentCaptor argument = ArgumentCaptor.forClass(IndexRequest.class); + + verify(consumer).accept(argument.capture()); + + final IndexRequest indexRequest = argument.getValue(); + final Map payloadMap = indexRequest.sourceAsMap(); + + assertThat(payloadMap, hasEntry("some key", "some value")); + } +}