diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/logging/AppLoggingUtils.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/logging/AppLoggingUtils.java index d5cbf0f474..271f651134 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/logging/AppLoggingUtils.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/logging/AppLoggingUtils.java @@ -6,8 +6,6 @@ */ package com.newrelic.agent.bridge.logging; -import com.newrelic.agent.bridge.logging.LogAttributeKey; -import com.newrelic.agent.bridge.logging.LogAttributeType; import com.newrelic.api.agent.NewRelic; import java.io.UnsupportedEncodingException; @@ -54,11 +52,32 @@ public class AppLoggingUtils { * @return agent linking metadata string blob */ public static String getLinkingMetadataBlob() { - Map agentLinkingMetadata = NewRelic.getAgent().getLinkingMetadata(); + return constructLinkingMetadataBlob(NewRelic.getAgent().getLinkingMetadata()); + } + + /** + * Gets a String representing the agent linking metadata in blob format: + * NR-LINKING|entity.guid|hostname|trace.id|span.id|entity.name| + * + * @param agentLinkingMetadata map of linking metadata + * @return agent linking metadata string blob + */ + public static String getLinkingMetadataBlobFromMap(Map agentLinkingMetadata) { + return constructLinkingMetadataBlob(agentLinkingMetadata); + } + + /** + * Constructs a String representing the agent linking metadata in blob format: + * NR-LINKING|entity.guid|hostname|trace.id|span.id|entity.name| + * + * @param agentLinkingMetadata map of linking metadata + * @return agent linking metadata string blob + */ + private static String constructLinkingMetadataBlob(Map agentLinkingMetadata) { StringBuilder blob = new StringBuilder(); blob.append(" ").append(BLOB_PREFIX).append(BLOB_DELIMITER); - if (agentLinkingMetadata != null && agentLinkingMetadata.size() > 0) { + if (agentLinkingMetadata != null && !agentLinkingMetadata.isEmpty()) { appendAttributeToBlob(agentLinkingMetadata.get(ENTITY_GUID), blob); appendAttributeToBlob(agentLinkingMetadata.get(HOSTNAME), blob); appendAttributeToBlob(agentLinkingMetadata.get(TRACE_ID), blob); diff --git a/instrumentation/apache-log4j-2/build.gradle b/instrumentation/apache-log4j-2/build.gradle index 2042aee68b..2083af1595 100644 --- a/instrumentation/apache-log4j-2/build.gradle +++ b/instrumentation/apache-log4j-2/build.gradle @@ -4,12 +4,11 @@ jar { dependencies { implementation(project(":agent-bridge")) - implementation("org.apache.logging.log4j:log4j-core:2.17.1") + implementation("org.apache.logging.log4j:log4j-core:2.20.0") } verifyInstrumentation { - passesOnly("org.apache.logging.log4j:log4j-core:[2.6,)") - excludeRegex '.*(alpha|beta|rc).*' + passesOnly("org.apache.logging.log4j:log4j-core:[2.6,3.0.0-alpha1)") } site { diff --git a/instrumentation/apache-log4j-2/src/main/java/com/nr/agent/instrumentation/log4j2/AgentUtil.java b/instrumentation/apache-log4j-2/src/main/java/com/nr/agent/instrumentation/log4j2/AgentUtil.java index df8761d0fc..8b8ffdcef3 100644 --- a/instrumentation/apache-log4j-2/src/main/java/com/nr/agent/instrumentation/log4j2/AgentUtil.java +++ b/instrumentation/apache-log4j-2/src/main/java/com/nr/agent/instrumentation/log4j2/AgentUtil.java @@ -14,7 +14,6 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.message.Message; -import org.apache.logging.log4j.util.ReadOnlyStringMap; import java.util.HashMap; import java.util.Map; @@ -128,4 +127,28 @@ private static int calculateInitialMapSize(Map mdcPropertyMap) { ? mdcPropertyMap.size() + DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES : DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES; } + + /** + * Checks pretty or compact JSON layout strings for a series of characters and returns the index of + * the characters or -1 if they were not found. This is used to find the log "message" substring + * so that the NR-LINKING metadata blob can be inserted when using local decorating with JsonLayout. + * + * @param writerString String representing JSON formatted log event + * @return positive int if index was found, else -1 + */ + public static int getIndexToModifyJson(String writerString) { + return writerString.indexOf("\",", writerString.indexOf("message")); + } + + /** + * Check if a valid match was found when calling String.indexOf. + * If index value is -1 then no valid match was found, a positive integer represents a valid index. + * + * @param indexToModifyJson int representing index returned by indexOf + * @return true if a valid index was found, else false + */ + public static boolean foundIndexToInsertLinkingMetadata(int indexToModifyJson) { + return indexToModifyJson != -1; + } + } diff --git a/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/LogEvent_Instrumentation.java b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/LogEvent_Instrumentation.java new file mode 100644 index 0000000000..4c86c4b000 --- /dev/null +++ b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/LogEvent_Instrumentation.java @@ -0,0 +1,80 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.logging.log4j.core; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.NewField; +import com.newrelic.api.agent.weaver.Weave; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.ThreadContext; +import org.apache.logging.log4j.core.impl.ThrowableProxy; +import org.apache.logging.log4j.core.time.Instant; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.util.ReadOnlyStringMap; + +import java.util.Map; + +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingLocalDecoratingEnabled; + +@Weave(originalName = "org.apache.logging.log4j.core.LogEvent", type = MatchType.Interface) +public abstract class LogEvent_Instrumentation { + + /* + * In cases where the LogEvent is sent to an AsyncAppender, getLinkingMetadata would get called on a new thread and the trace.id and span.id + * would be missing. To work around this we save the linking metadata on the LogEvent on the thread where it was created and use it later. + */ + @NewField + public Map agentLinkingMetadata = isApplicationLoggingLocalDecoratingEnabled() ? NewRelic.getAgent().getLinkingMetadata() : null; + + public abstract LogEvent toImmutable(); + + @Deprecated + public abstract Map getContextMap(); + + public abstract ReadOnlyStringMap getContextData(); + + public abstract ThreadContext.ContextStack getContextStack(); + + public abstract String getLoggerFqcn(); + + public abstract Level getLevel(); + + public abstract String getLoggerName(); + + public abstract Marker getMarker(); + + public abstract Message getMessage(); + + public abstract long getTimeMillis(); + + public abstract Instant getInstant(); + + public abstract StackTraceElement getSource(); + + public abstract String getThreadName(); + + public abstract long getThreadId(); + + public abstract int getThreadPriority(); + + public abstract Throwable getThrown(); + + public abstract ThrowableProxy getThrownProxy(); + + public abstract boolean isEndOfBatch(); + + public abstract boolean isIncludeLocation(); + + public abstract void setEndOfBatch(boolean endOfBatch); + + public abstract void setIncludeLocation(boolean locationRequired); + + public abstract long getNanoTime(); +} diff --git a/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/AbstractJacksonLayout_Instrumentation.java b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/AbstractJacksonLayout_Instrumentation.java new file mode 100644 index 0000000000..d24a77cb4a --- /dev/null +++ b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/AbstractJacksonLayout_Instrumentation.java @@ -0,0 +1,66 @@ +/* + * + * * Copyright 2023 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.logging.log4j.core.layout; + +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; +import org.apache.logging.log4j.core.LogEvent_Instrumentation; +import org.apache.logging.log4j.core.util.StringBuilderWriter; + +import java.io.IOException; +import java.io.Writer; + +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.BLOB_PREFIX; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.getLinkingMetadataBlob; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.getLinkingMetadataBlobFromMap; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingEnabled; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingLocalDecoratingEnabled; +import static com.nr.agent.instrumentation.log4j2.AgentUtil.foundIndexToInsertLinkingMetadata; +import static com.nr.agent.instrumentation.log4j2.AgentUtil.getIndexToModifyJson; + +@Weave(originalName = "org.apache.logging.log4j.core.layout.AbstractJacksonLayout", type = MatchType.ExactClass) +abstract class AbstractJacksonLayout_Instrumentation { + + public abstract void toSerializable(final LogEvent_Instrumentation event, final Writer writer) throws IOException; + + public String toSerializable(final LogEvent_Instrumentation event) { + final StringBuilderWriter writer = new StringBuilderWriter(); + try { + toSerializable(event, writer); + String writerString = writer.toString(); + String modified = writerString; + + // Append linking metadata to the log message if local decorating is enabled + if (isApplicationLoggingEnabled() && isApplicationLoggingLocalDecoratingEnabled()) { + // It is possible that the log might already have NR-LINKING metadata from JUL instrumentation + if (!writerString.contains(BLOB_PREFIX)) { + int indexToModifyJson = getIndexToModifyJson(writerString); + if (foundIndexToInsertLinkingMetadata(indexToModifyJson)) { + // Replace the JSON string with modified version that includes NR-LINKING metadata + if (event != null && event.agentLinkingMetadata != null && (!event.agentLinkingMetadata.isEmpty())) { + // Get linking metadata stored on LogEvent if available. This ensures that + // the trace.id and span.id will be available when using an async appender. + modified = new StringBuilder(writerString).insert(indexToModifyJson, getLinkingMetadataBlobFromMap(event.agentLinkingMetadata)) + .toString(); + } else { + // Get linking metadata from current thread if it is not available on LogEvent. + modified = new StringBuilder(writerString).insert(indexToModifyJson, getLinkingMetadataBlob()).toString(); + } + } + } + return modified; + } + + return writerString; + } catch (final IOException e) { + return Weaver.callOriginal(); + } + } + +} diff --git a/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder_Instrumentation.java b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder_Instrumentation.java index e09c3e19a5..ef3469d0d7 100644 --- a/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder_Instrumentation.java +++ b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder_Instrumentation.java @@ -11,6 +11,7 @@ import com.newrelic.api.agent.weaver.Weave; import com.newrelic.api.agent.weaver.Weaver; +import static com.newrelic.agent.bridge.logging.AppLoggingUtils.BLOB_PREFIX; import static com.newrelic.agent.bridge.logging.AppLoggingUtils.getLinkingMetadataBlob; import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingEnabled; import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingLocalDecoratingEnabled; @@ -30,11 +31,15 @@ public void encode(final StringBuilder source, final ByteBufferDestination desti } private void appendAgentMetadata(StringBuilder source) { - int breakLine = source.toString().lastIndexOf("\n"); - if (breakLine != -1) { - source.replace(breakLine, breakLine + 1, ""); + String sourceString = source.toString(); + // It is possible that the log might already have NR-LINKING metadata from JUL instrumentation + if (!sourceString.contains(BLOB_PREFIX)) { + int breakLine = sourceString.lastIndexOf("\n"); + if (breakLine != -1) { + source.replace(breakLine, breakLine + 1, ""); + } + source.append(getLinkingMetadataBlob()).append("\n"); } - source.append(getLinkingMetadataBlob()).append("\n"); } -} \ No newline at end of file +}