diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 91f6c3c008..e19ee448d5 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -20,8 +20,8 @@ endif::[] === Unreleased -[[release-notes-1.30.2]] -==== 1.30.2 - YYYY/MM/DD +[[release-notes-1.31.0]] +==== 1.31.0 - YYYY/MM/DD [float] ===== Potentially breaking changes @@ -40,6 +40,7 @@ allows for spans representing asynchronous handling of requests for which the co [float] ===== Features * Set the service version when using the ECS reformatting of the application logs: {pull}2603[#2603] +* Add ECS-reformatting support for `java.util.logging` - {pull}2591[#2591] [float] ===== Bug fixes diff --git a/apm-agent-core/src/main/java/co/elastic/apm/agent/logging/LoggingConfiguration.java b/apm-agent-core/src/main/java/co/elastic/apm/agent/logging/LoggingConfiguration.java index e3e22f5693..869a0c9fc4 100644 --- a/apm-agent-core/src/main/java/co/elastic/apm/agent/logging/LoggingConfiguration.java +++ b/apm-agent-core/src/main/java/co/elastic/apm/agent/logging/LoggingConfiguration.java @@ -155,7 +155,7 @@ static LogLevel mapLogLevel(LogLevel original) { .tags("added[1.22.0]", "experimental") .description("Specifying whether and how the agent should automatically reformat application logs \n" + "into {ecs-logging-ref}/intro.html[ECS-compatible JSON], suitable for ingestion into Elasticsearch for \n" + - "further Log analysis. This functionality is available for log4j1, log4j2 and Logback. \n" + + "further Log analysis. This functionality is available for log4j1, log4j2, Logback and `java.util.logging`. \n" + "The ECS log lines will include active trace/transaction/error IDs, if there are such. \n" + "\n" + "This option only applies to pattern layouts/formatters by default.\n" + @@ -210,7 +210,9 @@ static LogLevel mapLogLevel(LogLevel original) { .buildWithDefault(Arrays.asList( WildcardMatcher.valueOf("*PatternLayout*"), WildcardMatcher.valueOf("org.apache.log4j.SimpleLayout"), - WildcardMatcher.valueOf("ch.qos.logback.core.encoder.EchoEncoder") + WildcardMatcher.valueOf("ch.qos.logback.core.encoder.EchoEncoder"), + WildcardMatcher.valueOf("java.util.logging.SimpleFormatter"), + WildcardMatcher.valueOf("org.springframework.boot.logging.java.SimpleFormatter") )); private final ConfigurationOption logEcsFormattingDestinationDir = ConfigurationOption.stringOption() @@ -384,6 +386,10 @@ public long getLogFileSize() { return logFileSize.get().getBytes(); } + public long getDefaultLogFileSize() { + return logFileSize.getValueConverter().convert(logFileSize.getDefaultValueAsString()).getBytes(); + } + public boolean isShipAgentLogs() { return shipAgentLogs.get(); } diff --git a/apm-agent-plugins/apm-ecs-logging-plugin/pom.xml b/apm-agent-plugins/apm-ecs-logging-plugin/pom.xml index d1e2b4ed4e..67aeb7f828 100644 --- a/apm-agent-plugins/apm-ecs-logging-plugin/pom.xml +++ b/apm-agent-plugins/apm-ecs-logging-plugin/pom.xml @@ -19,7 +19,7 @@ co.elastic.logging log4j-ecs-layout - 1.2.0 + 1.4.0 provided diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/pom.xml b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/pom.xml new file mode 100644 index 0000000000..8ff4140a13 --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + apm-logging-plugin + co.elastic.apm + 1.30.2-SNAPSHOT + + + apm-jul-plugin + ${project.groupId}:${project.artifactId} + + + ${project.basedir}/../../.. + + + + + ${project.groupId} + apm-logging-plugin-common + ${project.version} + + + co.elastic.logging + jul-ecs-formatter + ${version.ecs.logging} + + + ${project.groupId} + apm-logging-plugin-common + ${project.version} + test-jar + test + + + + + + + maven-surefire-plugin + + + --add-opens java.logging/java.util.logging=ALL-UNNAMED + + + + + diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/JulPluginClassLoaderRootPackageCustomizer.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/JulPluginClassLoaderRootPackageCustomizer.java new file mode 100644 index 0000000000..b7d85a0e90 --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/JulPluginClassLoaderRootPackageCustomizer.java @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul; + +import co.elastic.apm.agent.loginstr.LoggingPluginClassLoaderRootPackageCustomizer; + +public class JulPluginClassLoaderRootPackageCustomizer extends LoggingPluginClassLoaderRootPackageCustomizer {} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/error/package-info.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/error/package-info.java new file mode 100644 index 0000000000..319066430e --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/error/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ +@NonnullApi +package co.elastic.apm.agent.jul.error; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulConsoleHandlerPublishAdvice.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulConsoleHandlerPublishAdvice.java new file mode 100644 index 0000000000..21267c4af0 --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulConsoleHandlerPublishAdvice.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul.reformatting; + +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +import java.util.logging.ConsoleHandler; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; + +public class JulConsoleHandlerPublishAdvice { + + private static final JulEcsReformattingHelper helper = new JulEcsReformattingHelper(); + + @SuppressWarnings("unused") + @Advice.OnMethodEnter(suppress = Throwable.class, skipOn = Advice.OnNonDefaultValue.class, inline = false) + public static boolean initializeReformatting(@Advice.This(typing = Assigner.Typing.DYNAMIC) ConsoleHandler thisHandler) { + return helper.onAppendEnter(thisHandler); + } + + @SuppressWarnings({"unused"}) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class, inline = false) + public static void reformatLoggingEvent(@Advice.Argument(value = 0, typing = Assigner.Typing.DYNAMIC) final LogRecord logRecord, + @Advice.This(typing = Assigner.Typing.DYNAMIC) ConsoleHandler thisHandler) { + + StreamHandler shadeAppender = helper.onAppendExit(thisHandler); + if (shadeAppender != null) { + shadeAppender.publish(logRecord); + } + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulEcsReformattingHelper.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulEcsReformattingHelper.java new file mode 100644 index 0000000000..32d41785d4 --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulEcsReformattingHelper.java @@ -0,0 +1,161 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul.reformatting; + +import co.elastic.apm.agent.loginstr.reformatting.AbstractEcsReformattingHelper; +import co.elastic.apm.agent.loginstr.reformatting.Utils; +import co.elastic.apm.agent.sdk.logging.Logger; +import co.elastic.apm.agent.sdk.logging.LoggerFactory; +import co.elastic.apm.agent.util.LoggerUtils; +import co.elastic.logging.AdditionalField; +import co.elastic.logging.jul.EcsFormatter; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.StreamHandler; + +class JulEcsReformattingHelper extends AbstractEcsReformattingHelper { + + private static final Logger logger = LoggerFactory.getLogger(JulEcsReformattingHelper.class); + private static final Logger oneTimeLogFileLimitWarningLogger = LoggerUtils.logOnce(logger); + + private static final ThreadLocal currentPattern = new ThreadLocal<>(); + private static final ThreadLocal currentExampleLogFile = new ThreadLocal<>(); + + JulEcsReformattingHelper() {} + + public boolean onAppendEnter(FileHandler fileHandler, String pattern, File exampleLogFile) { + try { + currentPattern.set(pattern); + currentExampleLogFile.set(exampleLogFile.toPath()); + return super.onAppendEnter(fileHandler); + } finally { + currentPattern.remove(); + currentExampleLogFile.remove(); + } + } + + @Nullable + @Override + protected Formatter getFormatterFrom(StreamHandler handler) { + return handler.getFormatter(); + } + + @Override + protected void setFormatter(StreamHandler handler, Formatter formatter) { + handler.setFormatter(formatter); + } + + @Override + protected String getAppenderName(StreamHandler handler) { + if (handler instanceof FileHandler) { + return "FILE"; + } else if (handler instanceof ConsoleHandler) { + return "CONSOLE"; + } else { + return handler.getClass().getSimpleName(); + } + } + + @Override + protected Formatter createEcsFormatter(String eventDataset, @Nullable String serviceName, @Nullable String serviceVersion, + @Nullable String serviceNodeName, @Nullable Map additionalFields, + Formatter originalFormatter) { + EcsFormatter ecsFormatter = new EcsFormatter(); + ecsFormatter.setServiceName(serviceName); + ecsFormatter.setServiceVersion(serviceVersion); + ecsFormatter.setServiceNodeName(serviceNodeName); + ecsFormatter.setEventDataset(eventDataset); + if (additionalFields != null && !additionalFields.isEmpty()) { + List additionalFieldList = new ArrayList<>(); + for (Map.Entry keyValuePair : additionalFields.entrySet()) { + additionalFieldList.add(new AdditionalField(keyValuePair.getKey(), keyValuePair.getValue())); + } + ecsFormatter.setAdditionalFields(additionalFieldList); + } + ecsFormatter.setIncludeOrigin(false); + ecsFormatter.setStackTraceAsArray(false); + return ecsFormatter; + } + + @Nullable + @Override + protected StreamHandler createAndStartEcsAppender(StreamHandler originalHandler, String ecsAppenderName, Formatter ecsFormatter) { + StreamHandler shadeHandler = null; + if (originalHandler instanceof FileHandler) { + try { + String pattern = computeEcsFileHandlerPattern( + currentPattern.get(), + currentExampleLogFile.get(), + getConfiguredReformattingDir(), + true + ); + // In earlier versions, there is only constructor with log file limit given as int, whereas in later ones there are + // overloads for both either int or long. Typically, this should be enough, but not necessarily + int maxLogFileSize = (int) getMaxLogFileSize(); + if ((long) maxLogFileSize != getMaxLogFileSize()) { + maxLogFileSize = (int) getDefaultMaxLogFileSize(); + oneTimeLogFileLimitWarningLogger.warn("Configured log max size ({} bytes) is too big for JUL settings, which " + + "use int to configure the file size limit. Consider reducing the log max size configuration to a value below " + + "Integer#MAX_VALUE. Defaulting to {} bytes.", getMaxLogFileSize(), maxLogFileSize); + } + shadeHandler = new FileHandler(pattern, maxLogFileSize, 2, true); + shadeHandler.setFormatter(ecsFormatter); + } catch (Exception e) { + logger.error("Failed to create Log shading FileAppender. Auto ECS reformatting will not work.", e); + } + } + return shadeHandler; + } + + static String computeEcsFileHandlerPattern(String pattern, Path originalFilePath, @Nullable String configuredReformattingDir, + boolean createDirs) throws IOException { + pattern = Utils.replaceFileExtensionToEcsJson(pattern); + // if the pattern does not contain rotation component, append one at the end + if (!pattern.contains("%g")) { + pattern = pattern + ".%g"; + } + int lastPathSeparatorIndex = pattern.lastIndexOf('/'); + if (lastPathSeparatorIndex > 0 && pattern.length() > lastPathSeparatorIndex) { + pattern = pattern.substring(lastPathSeparatorIndex + 1); + } + Path logReformattingDir = Utils.computeLogReformattingDir(originalFilePath, configuredReformattingDir); + if (logReformattingDir != null) { + if (createDirs && !Files.exists(logReformattingDir)) { + Files.createDirectories(logReformattingDir); + } + pattern = logReformattingDir.resolve(pattern).toString(); + } + return pattern; + } + + @Override + protected void closeShadeAppender(StreamHandler shadeHandler) { + shadeHandler.close(); + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulFileHandlerPublishAdvice.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulFileHandlerPublishAdvice.java new file mode 100644 index 0000000000..03780a4d11 --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulFileHandlerPublishAdvice.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul.reformatting; + +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +import java.io.File; +import java.util.logging.FileHandler; +import java.util.logging.LogRecord; +import java.util.logging.StreamHandler; + +public class JulFileHandlerPublishAdvice { + + private static final JulEcsReformattingHelper helper = new JulEcsReformattingHelper(); + + @SuppressWarnings("unused") + @Advice.OnMethodEnter(suppress = Throwable.class, skipOn = Advice.OnNonDefaultValue.class, inline = false) + public static boolean initializeReformatting(@Advice.This(typing = Assigner.Typing.DYNAMIC) FileHandler thisHandler, + @Advice.FieldValue("pattern") String pattern, + @Advice.FieldValue("files") File[] files) { + return helper.onAppendEnter(thisHandler, pattern, files[0]); + } + + @SuppressWarnings({"unused"}) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class, inline = false) + public static void reformatLoggingEvent(@Advice.Argument(value = 0, typing = Assigner.Typing.DYNAMIC) final LogRecord logRecord, + @Advice.This(typing = Assigner.Typing.DYNAMIC) FileHandler thisHandler) { + + StreamHandler shadeAppender = helper.onAppendExit(thisHandler); + if (shadeAppender != null) { + shadeAppender.publish(logRecord); + } + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulHandlerCloseAdvice.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulHandlerCloseAdvice.java new file mode 100644 index 0000000000..fb2e69439e --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulHandlerCloseAdvice.java @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul.reformatting; + +import net.bytebuddy.asm.Advice; +import net.bytebuddy.implementation.bytecode.assign.Assigner; + +import java.util.logging.StreamHandler; + +public class JulHandlerCloseAdvice { + + private static final JulEcsReformattingHelper helper = new JulEcsReformattingHelper(); + + @SuppressWarnings({"unused"}) + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class, inline = false) + public static void stopAppender(@Advice.This(typing = Assigner.Typing.DYNAMIC) StreamHandler thisHandler) { + helper.closeShadeAppenderFor(thisHandler); + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulLogReformattingInstrumentation.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulLogReformattingInstrumentation.java new file mode 100644 index 0000000000..6a8d2eec7f --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/JulLogReformattingInstrumentation.java @@ -0,0 +1,118 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul.reformatting; + +import co.elastic.apm.agent.loginstr.AbstractLogIntegrationInstrumentation; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +import java.util.Collection; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +public abstract class JulLogReformattingInstrumentation extends AbstractLogIntegrationInstrumentation { + + @Override + public Collection getInstrumentationGroupNames() { + Collection ret = super.getInstrumentationGroupNames(); + ret.add("jul-ecs"); + return ret; + } + + @Override + public ElementMatcher.Junction getClassLoaderMatcher() { + // todo: change when adding support for instrumentation of Tomcat and JBoss logging + return isBootstrapClassLoader(); + } + + /** + * Instruments {@link java.util.logging.FileHandler#publish(LogRecord)} + */ + public static class FileReformattingInstrumentation extends JulLogReformattingInstrumentation { + + @Override + public ElementMatcher getTypeMatcher() { + return named("java.util.logging.FileHandler"); + } + + /** + * Instrumenting {@link java.util.logging.FileHandler#publish(LogRecord)} + */ + @Override + public ElementMatcher getMethodMatcher() { + return named("publish").and(takesArgument(0, named("java.util.logging.LogRecord"))); + } + + @Override + public String getAdviceClassName() { + return "co.elastic.apm.agent.jul.reformatting.JulFileHandlerPublishAdvice"; + } + } + + /** + * Instruments {@link java.util.logging.ConsoleHandler#publish(LogRecord)} + */ + public static class ConsoleReformattingInstrumentation extends JulLogReformattingInstrumentation { + + @Override + public ElementMatcher getTypeMatcher() { + return named("java.util.logging.ConsoleHandler"); + } + + /** + * Instrumenting {@link java.util.logging.ConsoleHandler#publish(LogRecord)} + */ + @Override + public ElementMatcher getMethodMatcher() { + return named("publish").and(takesArgument(0, named("java.util.logging.LogRecord"))); + } + + @Override + public String getAdviceClassName() { + return "co.elastic.apm.agent.jul.reformatting.JulConsoleHandlerPublishAdvice"; + } + } + + public static class StopAppenderInstrumentation extends JulLogReformattingInstrumentation { + + @Override + public ElementMatcher getTypeMatcher() { + return named("java.util.logging.ConsoleHandler").or(named("java.util.logging.FileHandler")); + } + + /** + * Instrumenting {@link Handler#close()} + */ + @Override + public ElementMatcher getMethodMatcher() { + return named("close").and(takesArguments(0)); + } + + @Override + public String getAdviceClassName() { + return "co.elastic.apm.agent.jul.reformatting.JulHandlerCloseAdvice"; + } + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/package-info.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/package-info.java new file mode 100644 index 0000000000..0a71507d1f --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/java/co/elastic/apm/agent/jul/reformatting/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ +@NonnullApi +package co.elastic.apm.agent.jul.reformatting; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer new file mode 100644 index 0000000000..9241fd013a --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.bci.PluginClassLoaderRootPackageCustomizer @@ -0,0 +1 @@ +co.elastic.apm.agent.jul.JulPluginClassLoaderRootPackageCustomizer diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation new file mode 100644 index 0000000000..582843d242 --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/main/resources/META-INF/services/co.elastic.apm.agent.sdk.ElasticApmInstrumentation @@ -0,0 +1,6 @@ +# ECS re-formatting +co.elastic.apm.agent.jul.reformatting.JulLogReformattingInstrumentation$FileReformattingInstrumentation +co.elastic.apm.agent.jul.reformatting.JulLogReformattingInstrumentation$ConsoleReformattingInstrumentation +co.elastic.apm.agent.jul.reformatting.JulLogReformattingInstrumentation$StopAppenderInstrumentation + +# Error creation diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/JulInstrumentationTest.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/JulInstrumentationTest.java new file mode 100644 index 0000000000..568d98a36e --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/JulInstrumentationTest.java @@ -0,0 +1,166 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul; + +import co.elastic.apm.agent.loginstr.LoggingInstrumentationTest; +import co.elastic.apm.agent.loginstr.LoggerFacade; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class JulInstrumentationTest extends LoggingInstrumentationTest { + + @Override + protected LoggerFacade createLoggerFacade() { + return new JulLoggerFacade(); + } + + @Override + protected void waitForFileRolling() { + await().untilAsserted(() -> assertThat(new File(getLogReformattingFilePath()).length()).isEqualTo(0)); + } + + @Override + protected String getLogReformattingFilePath() { + // in JUL, the base file also gets the generation number 0 + return super.getLogReformattingFilePath() + ".0"; + } + + @Override + protected boolean isLogCorrelationSupported() { + return false; + } + + /** + * Custom log levels that match other logging frameworks + */ + private static class CustomLevel extends java.util.logging.Level { + + protected CustomLevel(final String name, final int value) { + super(name, value); + } + + public static final CustomLevel FATAL = new CustomLevel("FATAL", 1100); + public static final CustomLevel ERROR = new CustomLevel("ERROR", 1000); + public static final CustomLevel WARN = new CustomLevel("WARN", 900); + public static final CustomLevel INFO = new CustomLevel("INFO", 800); + public static final CustomLevel DEBUG = new CustomLevel("DEBUG", 500); + public static final CustomLevel TRACE = new CustomLevel("TRACE", 400); + } + + private static class JulLoggerFacade implements LoggerFacade { + + static { + try { + LogManager.getLogManager().readConfiguration(JulLoggerFacade.class.getClassLoader().getResourceAsStream("logging.properties")); + } catch (IOException e) { + throw new RuntimeException("Failed to read the JUL test configuration file"); + } + } + + private Logger julLogger; + + @Override + public void open() { + julLogger = Logger.getLogger("Test-File-Logger"); + if (Arrays.stream(julLogger.getHandlers()).noneMatch(handler -> handler instanceof FileHandler)) { + try { + // In case there is no FileHandler as it was removed through close(). + // The reason not to re-add through close() is that this deletes the file and sometimes we want to manually review the + // original log file after the test ends. + julLogger.addHandler(new FileHandler()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void close() { + // Closing and replacing the log file handler so that the log file would be rewritten + for (Handler handler : julLogger.getHandlers()) { + handler.close(); + julLogger.removeHandler(handler); + } + } + + @Override + public String getLogFilePath() { + Handler[] loggerHandlers = julLogger.getHandlers(); + for (Handler loggerHandler : loggerHandlers) { + if (loggerHandler instanceof FileHandler) { + // no API for that, so we use reflection for tests and the field in the instrumentation + try { + Field filesField = FileHandler.class.getDeclaredField("files"); + filesField.setAccessible(true); + File[] files = (File[]) filesField.get(loggerHandler); + return files[0].getAbsolutePath(); + } catch (Exception e) { + throw new RuntimeException("Failed to get log file path through reflection", e); + } + } + } + throw new IllegalStateException("Couldn't find a FileHandler for logger " + julLogger.getName()); + } + + @Override + public void trace(String message) { + julLogger.log(CustomLevel.TRACE, message); + } + + @Override + public void debug(String message) { + julLogger.log(CustomLevel.DEBUG, message); + } + + @Override + public void warn(String message) { + julLogger.log(CustomLevel.WARN, message); + } + + @Override + public void error(String message) { + julLogger.log(CustomLevel.ERROR, message); + } + + @Override + public void error(String message, Throwable throwable) { + julLogger.log(CustomLevel.ERROR, message, throwable); + } + + @Override + public void putTraceIdToMdc(String traceId) { + // not supported for JUL + } + + @Override + public void removeTraceIdFromMdc() { + // not supported for JUL + } + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/error/JulLoggerErrorCapturingInstrumentationTest.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/error/JulLoggerErrorCapturingInstrumentationTest.java new file mode 100644 index 0000000000..4bd53ca2e1 --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/error/JulLoggerErrorCapturingInstrumentationTest.java @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul.error; + +import co.elastic.apm.agent.loginstr.error.AbstractErrorLoggingInstrumentationTest; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.logging.Level; +import java.util.logging.Logger; + +// todo: add support and enable +@Disabled +public class JulLoggerErrorCapturingInstrumentationTest extends AbstractErrorLoggingInstrumentationTest { + + private static final Logger logger = Logger.getLogger(JulLoggerErrorCapturingInstrumentationTest.class.getName()); + + @Test + void captureErrorExceptionWithStringMessage() { + logger.log(Level.SEVERE, "exception captured", new RuntimeException("some business exception")); + verifyThatExceptionCaptured(1, "some business exception", RuntimeException.class); + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/package-info.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/package-info.java new file mode 100644 index 0000000000..3ef4e5109c --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/package-info.java @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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. + */ +@NonnullApi +package co.elastic.apm.agent.jul; + +import co.elastic.apm.agent.sdk.NonnullApi; diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/reformatting/JulEcsReformattingHelperTest.java b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/reformatting/JulEcsReformattingHelperTest.java new file mode 100644 index 0000000000..5f6051cafb --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/java/co/elastic/apm/agent/jul/reformatting/JulEcsReformattingHelperTest.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.jul.reformatting; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JulEcsReformattingHelperTest { + + @Test + void testEcsFileHandlerPatternComputation() throws IOException { + + assertThat(JulEcsReformattingHelper.computeEcsFileHandlerPattern( + "%t/test%g.log%u", + Path.of("/tmp/path/test0.log8"), + "reformat", + false) + ).isEqualTo("/tmp/path/reformat/test%g.ecs.json"); + + assertThat(JulEcsReformattingHelper.computeEcsFileHandlerPattern( + "/root/path/test%g.log%u", + Path.of("/root/path/test0.log8"), + "reformat", + false) + ).isEqualTo("/root/path/reformat/test%g.ecs.json"); + + assertThat(JulEcsReformattingHelper.computeEcsFileHandlerPattern( + "%t/test.log%u", + Path.of("/tmp/path/test.log8"), + "reformat", + false) + ).isEqualTo("/tmp/path/reformat/test.ecs.json.%g"); + + assertThat(JulEcsReformattingHelper.computeEcsFileHandlerPattern( + "test%g.log%u", + Path.of("test0.log8"), + "reformat", + false) + ).isEqualTo("reformat/test%g.ecs.json"); + + assertThat(JulEcsReformattingHelper.computeEcsFileHandlerPattern( + "%t/test%g.%u.log", + Path.of("/tmp/path/test0.8.log"), + null, + false) + ).isEqualTo("/tmp/path/test%g.%u.ecs.json"); + + assertThat(JulEcsReformattingHelper.computeEcsFileHandlerPattern( + "%t/test%g.%u.log", + Path.of("/tmp/path/test0.8.log"), + "", + false) + ).isEqualTo("/tmp/path/test%g.%u.ecs.json"); + } +} diff --git a/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/resources/logging.properties b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/resources/logging.properties new file mode 100644 index 0000000000..508fb42e2d --- /dev/null +++ b/apm-agent-plugins/apm-logging-plugin/apm-jul-plugin/src/test/resources/logging.properties @@ -0,0 +1,14 @@ +# Log file output +java.util.logging.FileHandler.pattern = target/jul.log +java.util.logging.FileHandler.limit = 50000 +java.util.logging.FileHandler.count = 1 +java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter + +java.util.logging.ConsoleHandler.level = FINER +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter + +java.util.logging.SimpleFormatter.format=%1$tFT%1$tT.%1$tLZ main %4$s %3$s %5$s %6$s %n + +# log level for package +Test-File-Logger.level = FINEST +Test-File-Logger.handlers = java.util.logging.FileHandler diff --git a/apm-agent-plugins/apm-logging-plugin/apm-log4j1-plugin/src/main/java/co/elastic/apm/agent/log4j1/reformatting/Log4j1AppenderAppendAdvice.java b/apm-agent-plugins/apm-logging-plugin/apm-log4j1-plugin/src/main/java/co/elastic/apm/agent/log4j1/reformatting/Log4j1AppenderAppendAdvice.java index 92d565a34b..04ed5f72dd 100644 --- a/apm-agent-plugins/apm-logging-plugin/apm-log4j1-plugin/src/main/java/co/elastic/apm/agent/log4j1/reformatting/Log4j1AppenderAppendAdvice.java +++ b/apm-agent-plugins/apm-logging-plugin/apm-log4j1-plugin/src/main/java/co/elastic/apm/agent/log4j1/reformatting/Log4j1AppenderAppendAdvice.java @@ -29,8 +29,7 @@ public class Log4j1AppenderAppendAdvice { @SuppressWarnings("unused") @Advice.OnMethodEnter(suppress = Throwable.class, skipOn = Advice.OnNonDefaultValue.class, inline = false) - public static boolean initializeReformatting(@Advice.Argument(value = 0, typing = Assigner.Typing.DYNAMIC) final LoggingEvent eventObject, - @Advice.This(typing = Assigner.Typing.DYNAMIC) WriterAppender thisAppender) { + public static boolean initializeReformatting(@Advice.This(typing = Assigner.Typing.DYNAMIC) WriterAppender thisAppender) { return helper.onAppendEnter(thisAppender); } diff --git a/apm-agent-plugins/apm-logging-plugin/apm-log4j2-plugin/src/main/java/co/elastic/apm/agent/log4j2/reformatting/Log4j2AppenderAppendAdvice.java b/apm-agent-plugins/apm-logging-plugin/apm-log4j2-plugin/src/main/java/co/elastic/apm/agent/log4j2/reformatting/Log4j2AppenderAppendAdvice.java index 002adc8a5e..7e403ff78b 100644 --- a/apm-agent-plugins/apm-logging-plugin/apm-log4j2-plugin/src/main/java/co/elastic/apm/agent/log4j2/reformatting/Log4j2AppenderAppendAdvice.java +++ b/apm-agent-plugins/apm-logging-plugin/apm-log4j2-plugin/src/main/java/co/elastic/apm/agent/log4j2/reformatting/Log4j2AppenderAppendAdvice.java @@ -29,8 +29,7 @@ public class Log4j2AppenderAppendAdvice { @SuppressWarnings("unused") @Advice.OnMethodEnter(suppress = Throwable.class, skipOn = Advice.OnNonDefaultValue.class, inline = false) - public static boolean initializeReformatting(@Advice.Argument(value = 0, typing = Assigner.Typing.DYNAMIC) final LogEvent eventObject, - @Advice.This(typing = Assigner.Typing.DYNAMIC) Appender thisAppender) { + public static boolean initializeReformatting(@Advice.This(typing = Assigner.Typing.DYNAMIC) Appender thisAppender) { return helper.onAppendEnter(thisAppender); } diff --git a/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/AbstractEcsReformattingHelper.java b/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/AbstractEcsReformattingHelper.java index 11fa6e4665..5b03d98223 100644 --- a/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/AbstractEcsReformattingHelper.java +++ b/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/AbstractEcsReformattingHelper.java @@ -382,7 +382,7 @@ public void closeShadeAppenderFor(A originalAppender) { /** * Checks whether the given appender is a shading appender, so to avoid recursive reformatting - * @return true if the provide appender is a shading appender; false otherwise + * @return true if the provided appender is a shading appender; false otherwise */ private boolean isShadingAppender(A appender) { //noinspection StringEquality @@ -479,5 +479,9 @@ protected long getMaxLogFileSize() { return loggingConfiguration.getLogFileSize(); } + protected long getDefaultMaxLogFileSize() { + return loggingConfiguration.getDefaultLogFileSize(); + } + protected abstract void closeShadeAppender(A shadeAppender); } diff --git a/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/Utils.java b/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/Utils.java index a4cd93969d..417af37fec 100644 --- a/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/Utils.java +++ b/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/main/java/co/elastic/apm/agent/loginstr/reformatting/Utils.java @@ -45,7 +45,7 @@ public static String computeLogReformattingFilePath(String originalLogFile, @Nul } @Nullable - private static Path computeLogReformattingDir(Path originalFilePath, @Nullable String configuredReformattingDestinationDir) { + public static Path computeLogReformattingDir(Path originalFilePath, @Nullable String configuredReformattingDestinationDir) { Path ecsDir; Path logsDir = originalFilePath.getParent(); if (configuredReformattingDestinationDir == null) { @@ -59,7 +59,7 @@ private static Path computeLogReformattingDir(Path originalFilePath, @Nullable S return ecsDir; } - static String replaceFileExtensionToEcsJson(String originalFileName) { + public static String replaceFileExtensionToEcsJson(String originalFileName) { int extensionIndex = originalFileName.lastIndexOf('.'); if (extensionIndex > 0) { originalFileName = originalFileName.substring(0, extensionIndex); diff --git a/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/test/java/co/elastic/apm/agent/loginstr/LoggingInstrumentationTest.java b/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/test/java/co/elastic/apm/agent/loginstr/LoggingInstrumentationTest.java index 05f96f45fa..3bb978ba80 100644 --- a/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/test/java/co/elastic/apm/agent/loginstr/LoggingInstrumentationTest.java +++ b/apm-agent-plugins/apm-logging-plugin/apm-logging-plugin-common/src/test/java/co/elastic/apm/agent/loginstr/LoggingInstrumentationTest.java @@ -193,7 +193,6 @@ public void testLazyEcsFileCreation() throws Exception { ArrayList rawLogLines = readRawLogLines(); assertThat(rawLogLines).hasSize(4); - assertThat(rawLogLines).hasSize(4); ArrayList ecsLogLines = readEcsLogFile(); assertThat(ecsLogLines).hasSize(2); @@ -301,15 +300,24 @@ private void verifyEcsLogLine(JsonNode ecsLogLineTree) { boolean isErrorLine = logLevel.textValue().equalsIgnoreCase("error"); assertThat(ecsLogLineTree.get("log.logger").textValue()).isEqualTo("Test-File-Logger"); assertThat(ecsLogLineTree.get("message")).isNotNull(); + verifyTracingMetadata(ecsLogLineTree); + if (isLogCorrelationSupported()) { + assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRACE_ID_MDC_KEY).textValue()).isEqualTo(transaction.getTraceContext().getTraceId().toString()); + assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRANSACTION_ID_MDC_KEY).textValue()).isEqualTo(transaction.getTraceContext().getTransactionId().toString()); + verifyErrorCaptureAndCorrelation(isErrorLine, ecsLogLineTree); + } else { + assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRACE_ID_MDC_KEY)).isNull(); + assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRANSACTION_ID_MDC_KEY)).isNull(); + } + } + + private void verifyTracingMetadata(JsonNode ecsLogLineTree) { assertThat(ecsLogLineTree.get("service.name").textValue()).isEqualTo(serviceName); assertThat(ecsLogLineTree.get("service.node.name").textValue()).isEqualTo(SERVICE_NODE_NAME); assertThat(ecsLogLineTree.get("event.dataset").textValue()).isEqualTo(serviceName + ".FILE"); assertThat(ecsLogLineTree.get("service.version").textValue()).isEqualTo("v42"); assertThat(ecsLogLineTree.get("some.field").textValue()).isEqualTo("some-value"); assertThat(ecsLogLineTree.get("another.field").textValue()).isEqualTo("another-value"); - assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRACE_ID_MDC_KEY).textValue()).isEqualTo(transaction.getTraceContext().getTraceId().toString()); - assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRANSACTION_ID_MDC_KEY).textValue()).isEqualTo(transaction.getTraceContext().getTransactionId().toString()); - verifyErrorCaptureAndCorrelation(isErrorLine, ecsLogLineTree); } private void verifyErrorCaptureAndCorrelation(boolean isErrorLine, JsonNode ecsLogLineTree) { @@ -335,6 +343,7 @@ private ArrayList readRawLogLines() throws IOException { rawLogLines = stream .map(line -> line.split("\\s+")) // ignoring lines related to Throwable logging + .filter(lineParts -> lineParts.length > 0) .filter(lineParts -> safelyParseDate(lineParts[0]) != null) .collect(Collectors.toCollection(ArrayList::new)); } @@ -369,18 +378,27 @@ private void verifyEcsFormat(String[] splitRawLogLine, JsonNode ecsLogLineTree) boolean isErrorLine = logLevel.textValue().equalsIgnoreCase("error"); assertThat(splitRawLogLine[3]).isEqualTo(ecsLogLineTree.get("log.logger").textValue()); assertThat(splitRawLogLine[4]).isEqualTo(ecsLogLineTree.get("message").textValue()); - assertThat(ecsLogLineTree.get("service.name").textValue()).isEqualTo(serviceName); - assertThat(ecsLogLineTree.get("service.node.name").textValue()).isEqualTo(SERVICE_NODE_NAME); - assertThat(ecsLogLineTree.get("event.dataset").textValue()).isEqualTo(serviceName + ".FILE"); - assertThat(ecsLogLineTree.get("service.version").textValue()).isEqualTo("v42"); - assertThat(ecsLogLineTree.get("some.field").textValue()).isEqualTo("some-value"); - JsonNode traceId = ecsLogLineTree.get(AbstractLogCorrelationHelper.TRACE_ID_MDC_KEY); - assertThat(traceId).withFailMessage("Logging correlation does not work as expected").isNotNull(); - assertThat(traceId.textValue()).isEqualTo(transaction.getTraceContext().getTraceId().toString()); - JsonNode transactionId = ecsLogLineTree.get(AbstractLogCorrelationHelper.TRANSACTION_ID_MDC_KEY); - assertThat(transactionId).withFailMessage("Logging correlation does not work as expected").isNotNull(); - assertThat(transactionId.textValue()).isEqualTo(transaction.getTraceContext().getTransactionId().toString()); - verifyErrorCaptureAndCorrelation(isErrorLine, ecsLogLineTree); + verifyTracingMetadata(ecsLogLineTree); + verifyLogCorrelation(ecsLogLineTree, isErrorLine); + } + + private void verifyLogCorrelation(JsonNode ecsLogLineTree, boolean isErrorLine) { + if (isLogCorrelationSupported()) { + JsonNode traceId = ecsLogLineTree.get(AbstractLogCorrelationHelper.TRACE_ID_MDC_KEY); + assertThat(traceId).withFailMessage("Logging correlation does not work as expected").isNotNull(); + assertThat(traceId.textValue()).isEqualTo(transaction.getTraceContext().getTraceId().toString()); + JsonNode transactionId = ecsLogLineTree.get(AbstractLogCorrelationHelper.TRANSACTION_ID_MDC_KEY); + assertThat(transactionId).withFailMessage("Logging correlation does not work as expected").isNotNull(); + assertThat(transactionId.textValue()).isEqualTo(transaction.getTraceContext().getTransactionId().toString()); + verifyErrorCaptureAndCorrelation(isErrorLine, ecsLogLineTree); + } else { + assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRACE_ID_MDC_KEY)).isNull(); + assertThat(ecsLogLineTree.get(AbstractLogCorrelationHelper.TRANSACTION_ID_MDC_KEY)).isNull(); + } + } + + protected boolean isLogCorrelationSupported() { + return true; } /** @@ -411,7 +429,14 @@ public void testReformattedLogRolling() throws IOException { // log4j1 this happens AFTER the event is logged. This means we can only count on the non-active file to // contain a single line String ecsLogFilePath = getLogReformattingFilePath(); - ArrayList jsonNodes = TestUtils.readJsonFile(ecsLogFilePath + ".1"); + // in JUL, the base file is also denoted with a number (0) + if (ecsLogFilePath.endsWith(".0")) { + ecsLogFilePath = ecsLogFilePath.substring(0, ecsLogFilePath.length() - 2); + } + if (!ecsLogFilePath.endsWith(".1")) { + ecsLogFilePath = ecsLogFilePath + ".1"; + } + ArrayList jsonNodes = TestUtils.readJsonFile(ecsLogFilePath); assertThat(jsonNodes).hasSize(1); } diff --git a/apm-agent-plugins/apm-logging-plugin/pom.xml b/apm-agent-plugins/apm-logging-plugin/pom.xml index 26e81ecbeb..0a9aa560d8 100644 --- a/apm-agent-plugins/apm-logging-plugin/pom.xml +++ b/apm-agent-plugins/apm-logging-plugin/pom.xml @@ -24,6 +24,7 @@ apm-log4j2-plugin apm-jboss-logging-plugin apm-slf4j-plugin + apm-jul-plugin diff --git a/apm-agent/pom.xml b/apm-agent/pom.xml index b13b25b053..d3a2bae121 100644 --- a/apm-agent/pom.xml +++ b/apm-agent/pom.xml @@ -201,6 +201,11 @@ apm-slf4j-plugin ${project.version} + + ${project.groupId} + apm-jul-plugin + ${project.version} + ${project.groupId} apm-micrometer-plugin diff --git a/docs/configuration.asciidoc b/docs/configuration.asciidoc index d68f105809..83fc84edcc 100644 --- a/docs/configuration.asciidoc +++ b/docs/configuration.asciidoc @@ -761,7 +761,7 @@ you should add an additional entry to this list (make sure to also include the d ==== `enable_instrumentations` (added[1.28.0]) A list of instrumentations which should be selectively enabled. -Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. When set to non-empty value, only listed instrumentations will be enabled if they are not disabled through <> or <>. When not set or empty (default), all instrumentations enabled by default will be enabled unless they are disabled through <> or <>. @@ -789,7 +789,7 @@ NOTE: Changing this value at runtime can slow down the application temporarily. ==== `disable_instrumentations` (added[1.0.0,Changing this value at runtime is possible since version 1.15.0]) A list of instrumentations which should be disabled. -Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. For version `1.25.0` and later, use <> to enable experimental instrumentations. NOTE: Changing this value at runtime can slow down the application temporarily. @@ -1879,7 +1879,7 @@ NOTE: This feature is currently experimental, which means it is disabled by defa Specifying whether and how the agent should automatically reformat application logs into {ecs-logging-ref}/intro.html[ECS-compatible JSON], suitable for ingestion into Elasticsearch for -further Log analysis. This functionality is available for log4j1, log4j2 and Logback. +further Log analysis. This functionality is available for log4j1, log4j2, Logback and `java.util.logging`. The ECS log lines will include active trace/transaction/error IDs, if there are such. This option only applies to pattern layouts/formatters by default. @@ -1970,7 +1970,7 @@ Prepending an element with `(?-i)` makes the matching case sensitive. [options="header"] |============ | Default | Type | Dynamic -| `*PatternLayout*, org.apache.log4j.SimpleLayout, ch.qos.logback.core.encoder.EchoEncoder` | List | false +| `*PatternLayout*, org.apache.log4j.SimpleLayout, ch.qos.logback.core.encoder.EchoEncoder, java.util.logging.SimpleFormatter, org.springframework.boot.logging.java.SimpleFormatter` | List | false |============ @@ -3152,7 +3152,7 @@ Example: `5ms`. # sanitize_field_names=password,passwd,pwd,secret,*key,*token*,*session*,*credit*,*card*,*auth*,set-cookie # A list of instrumentations which should be selectively enabled. -# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. # When set to non-empty value, only listed instrumentations will be enabled if they are not disabled through <> or <>. # When not set or empty (default), all instrumentations enabled by default will be enabled unless they are disabled through <> or <>. # @@ -3165,7 +3165,7 @@ Example: `5ms`. # enable_instrumentations= # A list of instrumentations which should be disabled. -# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. +# Valid options are `annotations`, `annotations-capture-span`, `annotations-capture-transaction`, `annotations-traced`, `apache-commons-exec`, `apache-httpclient`, `asynchttpclient`, `aws-lambda`, `cassandra`, `concurrent`, `dubbo`, `elasticsearch-restclient`, `exception-handler`, `executor`, `executor-collection`, `experimental`, `fork-join`, `grails`, `grpc`, `hibernate-search`, `http-client`, `jakarta-websocket`, `javalin`, `javax-websocket`, `jax-rs`, `jax-ws`, `jboss-logging-correlation`, `jdbc`, `jdk-httpclient`, `jdk-httpserver`, `jedis`, `jms`, `jsf`, `jul-ecs`, `kafka`, `lettuce`, `log4j1-correlation`, `log4j1-ecs`, `log4j1-error`, `log4j2-correlation`, `log4j2-ecs`, `log4j2-error`, `logback-correlation`, `logback-ecs`, `logging`, `micrometer`, `mongodb-client`, `okhttp`, `opentelemetry`, `opentracing`, `process`, `public-api`, `quartz`, `rabbitmq`, `reactor`, `redis`, `redisson`, `render`, `scala-future`, `scheduled`, `servlet-api`, `servlet-api-async`, `servlet-api-dispatch`, `servlet-input-stream`, `slf4j-error`, `sparkjava`, `spring-amqp`, `spring-mvc`, `spring-resttemplate`, `spring-service-name`, `spring-view-render`, `spring-webflux`, `ssl-context`, `struts`, `timer-task`, `urlconnection`, `vertx`, `vertx-web`, `vertx-webclient`, `websocket`. # For version `1.25.0` and later, use <> to enable experimental instrumentations. # # NOTE: Changing this value at runtime can slow down the application temporarily. @@ -3774,7 +3774,7 @@ Example: `5ms`. # Specifying whether and how the agent should automatically reformat application logs # into {ecs-logging-ref}/intro.html[ECS-compatible JSON], suitable for ingestion into Elasticsearch for -# further Log analysis. This functionality is available for log4j1, log4j2 and Logback. +# further Log analysis. This functionality is available for log4j1, log4j2, Logback and `java.util.logging`. # The ECS log lines will include active trace/transaction/error IDs, if there are such. # # This option only applies to pattern layouts/formatters by default. @@ -3831,9 +3831,9 @@ Example: `5ms`. # # This setting can not be changed at runtime. Changes require a restart of the application. # Type: comma separated list -# Default value: *PatternLayout*,org.apache.log4j.SimpleLayout,ch.qos.logback.core.encoder.EchoEncoder +# Default value: *PatternLayout*,org.apache.log4j.SimpleLayout,ch.qos.logback.core.encoder.EchoEncoder,java.util.logging.SimpleFormatter,org.springframework.boot.logging.java.SimpleFormatter # -# log_ecs_formatter_allow_list=*PatternLayout*,org.apache.log4j.SimpleLayout,ch.qos.logback.core.encoder.EchoEncoder +# log_ecs_formatter_allow_list=*PatternLayout*,org.apache.log4j.SimpleLayout,ch.qos.logback.core.encoder.EchoEncoder,java.util.logging.SimpleFormatter,org.springframework.boot.logging.java.SimpleFormatter # If <> is set to `SHADE` or `REPLACE`, # the shade log files will be written alongside the original logs in the same directory by default. diff --git a/docs/supported-technologies.asciidoc b/docs/supported-technologies.asciidoc index a2c56e089e..ec724d001b 100644 --- a/docs/supported-technologies.asciidoc +++ b/docs/supported-technologies.asciidoc @@ -548,6 +548,12 @@ entries for `trace.id`, `transaction.id` and `error.id`. JBoss LogManager - 1.30.0 +|JUL - `java.util.logging` +|All supported Java versions +|When <> is enabled, logs will be automatically reformatted into +ECS-compatible format +|1.31.0 + |=== [float]