Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Log4j2 JsonLayout support #1559

Merged
merged 15 commits into from Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;
Expand Down Expand Up @@ -54,11 +52,32 @@ public class AppLoggingUtils {
* @return agent linking metadata string blob
*/
public static String getLinkingMetadataBlob() {
Map<String, String> 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<String, String> 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<String, String> 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);
Expand Down
23 changes: 23 additions & 0 deletions instrumentation/apache-log4j-2.11/build.gradle
@@ -0,0 +1,23 @@
jar {
manifest {
attributes 'Implementation-Title': 'com.newrelic.instrumentation.apache-log4j-2.11',
// The module was renamed to its current name from the name below. The alias exists so the instrumentation
// is still excluded/included for customers who have the old name in their configuration.
'Implementation-Title-Alias': 'com.newrelic.instrumentation.apache-log4j-2'
}
}

dependencies {
implementation(project(":agent-bridge"))
implementation("org.apache.logging.log4j:log4j-core:2.20.0")
}

verifyInstrumentation {
passesOnly("org.apache.logging.log4j:log4j-core:[2.11.0,)")
excludeRegex '.*(alpha|beta|rc).*'
}

site {
title 'Log4j2'
type 'Framework'
}
@@ -0,0 +1,155 @@
/*
*
* * Copyright 2022 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.agent.instrumentation.log4j2;

import com.newrelic.agent.bridge.AgentBridge;
import com.newrelic.agent.bridge.logging.AppLoggingUtils;
import com.newrelic.agent.bridge.logging.LogAttributeKey;
import com.newrelic.agent.bridge.logging.LogAttributeType;
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;

import static com.newrelic.agent.bridge.logging.AppLoggingUtils.DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.ERROR_CLASS;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.ERROR_MESSAGE;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.ERROR_STACK;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.INSTRUMENTATION;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.LEVEL;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.LOGGER_FQCN;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.LOGGER_NAME;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.MESSAGE;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.THREAD_ID;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.THREAD_NAME;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.TIMESTAMP;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.UNKNOWN;

public class AgentUtil {
/**
* Record a LogEvent to be sent to New Relic.
*
* @param event to parse
*/
public static void recordNewRelicLogEvent(LogEvent event) {
if (event != null) {
Message message = event.getMessage();
Throwable throwable = event.getThrown();

if (shouldCreateLogEvent(message, throwable)) {
ReadOnlyStringMap contextData = event.getContextData();
Map<LogAttributeKey, Object> logEventMap = new HashMap<>(calculateInitialMapSize(contextData));
logEventMap.put(INSTRUMENTATION, "apache-log4j-2.11");
if (message != null) {
String formattedMessage = message.getFormattedMessage();
if (formattedMessage != null && !formattedMessage.isEmpty()) {
logEventMap.put(MESSAGE, formattedMessage);
}
}
logEventMap.put(TIMESTAMP, event.getTimeMillis());

if (AppLoggingUtils.isAppLoggingContextDataEnabled() && contextData != null) {
for (Map.Entry<String, String> entry : contextData.toMap().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
LogAttributeKey logAttrKey = new LogAttributeKey(key, LogAttributeType.CONTEXT);
logEventMap.put(logAttrKey, value);
}
}

Level level = event.getLevel();
if (level != null) {
String levelName = level.name();
if (levelName.isEmpty()) {
logEventMap.put(LEVEL, UNKNOWN);
} else {
logEventMap.put(LEVEL, levelName);
}
}

String errorStack = ExceptionUtil.getErrorStack(throwable);
if (errorStack != null) {
logEventMap.put(ERROR_STACK, errorStack);
}

String errorMessage = ExceptionUtil.getErrorMessage(throwable);
if (errorMessage != null) {
logEventMap.put(ERROR_MESSAGE, errorMessage);
}

String errorClass = ExceptionUtil.getErrorClass(throwable);
if (errorClass != null) {
logEventMap.put(ERROR_CLASS, errorClass);
}

String threadName = event.getThreadName();
if (threadName != null) {
logEventMap.put(THREAD_NAME, threadName);
}

logEventMap.put(THREAD_ID, event.getThreadId());

String loggerName = event.getLoggerName();
if (loggerName != null) {
logEventMap.put(LOGGER_NAME, loggerName);
}

String loggerFqcn = event.getLoggerFqcn();
if (loggerFqcn != null) {
logEventMap.put(LOGGER_FQCN, loggerFqcn);
}

AgentBridge.getAgent().getLogSender().recordLogEvent(logEventMap);
}
}
}

/**
* A LogEvent MUST NOT be reported if neither a log message nor an error is logged. If either is present report the LogEvent.
*
* @param message Message to validate
* @param throwable Throwable to validate
* @return true if a LogEvent should be created, otherwise false
*/
private static boolean shouldCreateLogEvent(Message message, Throwable throwable) {
return (message != null) || !ExceptionUtil.isThrowableNull(throwable);
}

private static int calculateInitialMapSize(ReadOnlyStringMap mdcPropertyMap) {
return AppLoggingUtils.isAppLoggingContextDataEnabled() && mdcPropertyMap != null
? 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;
}

}
@@ -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<String, String> agentLinkingMetadata = isApplicationLoggingLocalDecoratingEnabled() ? NewRelic.getAgent().getLinkingMetadata() : null;

public abstract LogEvent toImmutable();

@Deprecated
public abstract Map<String, String> 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();
}
@@ -0,0 +1,66 @@
/*
*
* * Copyright 2022 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package org.apache.logging.log4j.core.config;

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 com.newrelic.api.agent.weaver.WeaveAllConstructors;
import com.newrelic.api.agent.weaver.Weaver;
import org.apache.logging.log4j.core.LogEvent;

import java.util.concurrent.atomic.AtomicBoolean;

import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingEnabled;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingForwardingEnabled;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.isApplicationLoggingMetricsEnabled;
import static com.nr.agent.instrumentation.log4j2.AgentUtil.recordNewRelicLogEvent;

@Weave(originalName = "org.apache.logging.log4j.core.config.LoggerConfig", type = MatchType.ExactClass)
public class LoggerConfig_Instrumentation {
@NewField
public static AtomicBoolean instrumented = new AtomicBoolean(false);

@WeaveAllConstructors
public LoggerConfig_Instrumentation() {
// Generate the instrumentation module supportability metric only once
if (!instrumented.getAndSet(true)) {
NewRelic.incrementCounter("Supportability/Logging/Java/Log4j2.11/enabled");
}
}

protected void callAppenders(LogEvent event) {
// Do nothing if application_logging.enabled: false
if (isApplicationLoggingEnabled()) {
// Do nothing if logger has parents and isAdditive is set to true to avoid duplicated counters and logs
if (getParent() == null || !isAdditive()) {
if (isApplicationLoggingMetricsEnabled()) {
// Generate log level metrics
NewRelic.incrementCounter("Logging/lines");
NewRelic.incrementCounter("Logging/lines/" + event.getLevel().toString());
}

if (isApplicationLoggingForwardingEnabled()) {
// Record and send LogEvent to New Relic
recordNewRelicLogEvent(event);
}
}
}
Weaver.callOriginal();
}

public LoggerConfig getParent() {
return Weaver.callOriginal();
}

public boolean isAdditive() {
return Weaver.callOriginal();
}

}