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

Feature/add tags to forwarded logs #1177

Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
*/
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;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import static java.util.Collections.emptyMap;

public class AppLoggingUtils {
public static final int DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES = 11;
// Log message attributes
Expand All @@ -40,6 +40,7 @@ public class AppLoggingUtils {
public static final String SPAN_ID = "span.id";
// Log attribute prefixes
public static final String CONTEXT_DATA_ATTRIBUTE_PREFIX = "context.";
public static final String TAGS_ATTRIBUTE_PREFIX = "tags.";
// Enabled defaults
private static final boolean APP_LOGGING_DEFAULT_ENABLED = true;
private static final boolean APP_LOGGING_METRICS_DEFAULT_ENABLED = true;
Expand Down Expand Up @@ -139,4 +140,8 @@ public static boolean isAppLoggingContextDataEnabled() {
return NewRelic.getAgent().getConfig().getValue("application_logging.forwarding.context_data.enabled",
APP_LOGGING_FORWARDING_INCLUDE_CONTEXT_DATA_DEFAULT_ENABLED);
}

public static Map<String, String> getTags() {
return NewRelic.getAgent().getConfig().getValue("labels", emptyMap());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public String applyPrefix(String key) {
return key;
}
},
CONTEXT(AppLoggingUtils.CONTEXT_DATA_ATTRIBUTE_PREFIX);
CONTEXT(AppLoggingUtils.CONTEXT_DATA_ATTRIBUTE_PREFIX),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extending the changes introduce by @meiao who introduced MDC support

TAG(AppLoggingUtils.TAGS_ATTRIBUTE_PREFIX);

private final String prefix;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@
*/
package com.newrelic.agent.bridge.logging;

import com.newrelic.agent.bridge.logging.AppLoggingUtils;
import org.junit.Assert;
import org.junit.Test;

import static org.mockito.ArgumentMatchers.any;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tidying up some unused imports

import static org.mockito.ArgumentMatchers.eq;

public class AppLoggingUtilsTest {

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ private static void generateLogMetrics(LoggingEvent event) {
private static void recordNewRelicLogEvent(LoggingEvent event) {
if (shouldCreateNewRelicLogEventFor(event)) {
boolean isAppLoggingContextDataEnabled = AppLoggingUtils.isAppLoggingContextDataEnabled();
Map<LogAttributeKey, Object> logEventMap = LoggingEventMap.from(event, isAppLoggingContextDataEnabled);
Map<String, String> tags = AppLoggingUtils.getTags();
Map<LogAttributeKey, Object> logEventMap = LoggingEventMap.from(event, isAppLoggingContextDataEnabled, tags);
AgentBridge.getAgent().getLogSender().recordLogEvent(logEventMap);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,104 +17,124 @@
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.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;
import static com.newrelic.agent.bridge.logging.AppLoggingUtils.INSTRUMENTATION;

class LoggingEventMap {
static Map<LogAttributeKey, Object> from(LoggingEvent event, boolean appLoggingContextDataEnabled) {
Map<LogAttributeKey, Object> logEventMap = initialMapWithMdcIfEnabled(event, appLoggingContextDataEnabled);

static Map<LogAttributeKey, Object> from(LoggingEvent event, boolean appLoggingContextDataEnabled, Map<String, String> tags) {
Map<LogAttributeKey, Object> logEventMap = initialMapWithMdcIfEnabled(event, appLoggingContextDataEnabled, tags);
logEventMap.put(INSTRUMENTATION, "apache-log4j-1");
addTags(tags, logEventMap);
addLoggerInfo(event, logEventMap);
addMessageAndTs(event, logEventMap);
addMessageAndTimestamp(event, logEventMap);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a rename - I felt that Ts was not really clear enough

addLevel(event, logEventMap);
addThreadInfo(event, logEventMap);
addErrorInfo(event, logEventMap);
return logEventMap;
}

private static Map<LogAttributeKey, Object> initialMapWithMdcIfEnabled(LoggingEvent event, boolean isAppLoggingContextDataEnabled) {
private static Map<LogAttributeKey, Object> initialMapWithMdcIfEnabled(LoggingEvent event, boolean isAppLoggingContextDataEnabled, Map<String, String> tags) {
Map<LogAttributeKey, Object> loggingEventMap = null;
int potentialMapEntries = DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES + tags.size();
if (isAppLoggingContextDataEnabled) {
Map<?, ?> mdc = event.getProperties();
if (mdc != null && !mdc.isEmpty()) {
loggingEventMap = new HashMap<>(AppLoggingUtils.DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES + mdc.size());
loggingEventMap = new HashMap<>(potentialMapEntries + mdc.size());
addMdc(mdc, loggingEventMap);
}
}
if (loggingEventMap == null) {
loggingEventMap = new HashMap<>(AppLoggingUtils.DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES);
loggingEventMap = new HashMap<>(potentialMapEntries);
}
return loggingEventMap;
}

private static void addMdc(Map<?, ?> mdc, Map<LogAttributeKey, Object> map) {
for (Map.Entry<?, ?> entry : mdc.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
private static void addMdc(Map<?, ?> mdc, Map<LogAttributeKey, Object> logEventMap) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@meiao log4j-2 instrumentation code uses the name logEventMap which is clearer, so have applied that name consistently across the logging instrumentation.

mdc.forEach((key, value) -> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note that Java 8 is the earliest supported version for current releases of the agent, so just using slightly more succinct syntax. Same change in all the logging instrumentation.

LogAttributeKey logAttrKey = new LogAttributeKey(key.toString(), LogAttributeType.CONTEXT);
map.put(logAttrKey, value);
}
logEventMap.put(logAttrKey, value);
});
}

private static void addMessageAndTs(LoggingEvent event, Map<LogAttributeKey, Object> map) {
private static void addMessageAndTimestamp(LoggingEvent event, Map<LogAttributeKey, Object> logEventMap) {
String message = event.getRenderedMessage();
if (message != null && !message.isEmpty()) {
map.put(AppLoggingUtils.MESSAGE, message);
logEventMap.put(MESSAGE, message);
}
map.put(AppLoggingUtils.TIMESTAMP, event.getTimeStamp());
logEventMap.put(TIMESTAMP, event.getTimeStamp());
}

private static void addErrorInfo(LoggingEvent event, Map<LogAttributeKey, Object> map) {
Throwable throwable = null;
private static void addErrorInfo(LoggingEvent event, Map<LogAttributeKey, Object> logEventMap) {
Throwable thrown = null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small name change - the type is Throwable but the variable is something that has been thrown

ThrowableInformation throwableInformation = event.getThrowableInformation();
if (throwableInformation != null) {
throwable = throwableInformation.getThrowable();
thrown = throwableInformation.getThrowable();
}

String errorMessage = Log4j1ExceptionUtil.getErrorMessage(throwable);
String errorMessage = Log4j1ExceptionUtil.getErrorMessage(thrown);
if (errorMessage != null) {
map.put(AppLoggingUtils.ERROR_MESSAGE, errorMessage);
logEventMap.put(ERROR_MESSAGE, errorMessage);
}

String errorStack = Log4j1ExceptionUtil.getErrorStack(throwable);
String errorStack = Log4j1ExceptionUtil.getErrorStack(thrown);
if (errorStack != null) {
map.put(AppLoggingUtils.ERROR_STACK, errorStack);
logEventMap.put(ERROR_STACK, errorStack);
}

String errorClass = Log4j1ExceptionUtil.getErrorClass(throwable);
String errorClass = Log4j1ExceptionUtil.getErrorClass(thrown);
if (errorClass != null) {
map.put(AppLoggingUtils.ERROR_CLASS, errorClass);
logEventMap.put(ERROR_CLASS, errorClass);
}
}

private static void addLevel(LoggingEvent event, Map<LogAttributeKey, Object> map) {
private static void addLevel(LoggingEvent event, Map<LogAttributeKey, Object> logEventMap) {
Level level = event.getLevel();
if (level != null) {
String levelName = level.toString();
if (levelName.isEmpty()) {
map.put(AppLoggingUtils.LEVEL, AppLoggingUtils.UNKNOWN);
logEventMap.put(LEVEL, UNKNOWN);
} else {
map.put(AppLoggingUtils.LEVEL, levelName);
logEventMap.put(LEVEL, levelName);
}
}
}

private static void addThreadInfo(LoggingEvent event, Map<LogAttributeKey, Object> map) {
private static void addThreadInfo(LoggingEvent event, Map<LogAttributeKey, Object> logEventMap) {
String threadName = event.getThreadName();
if (threadName != null) {
map.put(AppLoggingUtils.THREAD_NAME, threadName);
logEventMap.put(THREAD_NAME, threadName);
}
map.put(AppLoggingUtils.THREAD_ID, Thread.currentThread().getId());
logEventMap.put(THREAD_ID, Thread.currentThread().getId());
}

private static void addLoggerInfo(LoggingEvent event, Map<LogAttributeKey, Object> map) {
private static void addLoggerInfo(LoggingEvent event, Map<LogAttributeKey, Object> logEventMap) {
String loggerName = event.getLoggerName();
if (loggerName != null) {
map.put(AppLoggingUtils.LOGGER_NAME, loggerName);
logEventMap.put(LOGGER_NAME, loggerName);
}

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

private static void addTags(Map<String, String> tags, Map<LogAttributeKey, Object> logEventMap) {
tags.forEach((key, value) -> {
LogAttributeKey logAttrKey = new LogAttributeKey(key, LogAttributeType.TAG);
logEventMap.put(logAttrKey, value);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

package com.nr.agent.instrumentation.log4j1;

import com.newrelic.agent.bridge.logging.AppLoggingUtils;
import com.newrelic.agent.bridge.logging.LogAttributeKey;
import com.newrelic.agent.bridge.logging.LogAttributeType;
import org.apache.log4j.Category;
Expand All @@ -22,19 +21,34 @@
import java.util.Map;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;
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.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 java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;


public class LoggingEventMapTest {

private static Stream<Arguments> providerParamsForLoggingEventMapTest() {
return Stream.of(
argumentsOf("Minimal case", null, null, null, null, null, false, 4),
argumentsOf("Minimal case", null, null, null, null, null, false, emptyMap(), 4),
argumentsOf("Base case with all data MDC disabled", "com.newrelic.SomeClass", "SomeClasLogger",
Priority.ERROR, "Hello", new RuntimeException("SIMULATED"), false, 11),
argumentsOf("Minimal case with MDC enabled", null, null, null, null, null, true, 5),
Priority.ERROR, "Hello", new RuntimeException("SIMULATED"), false, singletonMap("tagKey", "tagValue"), 12),
argumentsOf("Minimal case with MDC enabled", null, null, null, null, null, true, emptyMap(), 6),
argumentsOf("Base case with all data and MDC enabled", "com.newrelic.OtherClass", "OtherClasLogger",
Priority.ERROR, "Hello", new RuntimeException("SIMULATED"), true, 12)
Priority.ERROR, "Hello", new RuntimeException("SIMULATED"), true, singletonMap("tagKey", "tagValue"), 13)
);
}

Expand All @@ -46,10 +60,11 @@ private static Arguments argumentsOf(
String message,
Throwable t,
boolean appLoggingContextDataEnabled,
Map<String, String> tags,
int expectedEntryCount

) {
return Arguments.of(testDetails, fqnOfLogger, loggerName, level, message, t, appLoggingContextDataEnabled,
return Arguments.of(testDetails, fqnOfLogger, loggerName, level, message, t, appLoggingContextDataEnabled, tags,
expectedEntryCount);
}

Expand All @@ -63,6 +78,7 @@ void testLoggingEventCreation(
String message,
Throwable t,
boolean appLoggingContextDataEnabled,
Map<String, String> tags,
int expectedEntryCount
) {
// setup Category with test factory
Expand All @@ -75,41 +91,41 @@ void testLoggingEventCreation(
MDC.put("some", "value");

// when logging event map created
Map<LogAttributeKey, Object> loggingEventMap = LoggingEventMap.from(event, appLoggingContextDataEnabled);
Map<LogAttributeKey, Object> loggingEventMap = LoggingEventMap.from(event, appLoggingContextDataEnabled, tags);

// then it is not null or empty, and it has expected size
assertNotNull(loggingEventMap);
assertFalse(loggingEventMap.isEmpty());
assertEquals(expectedEntryCount, loggingEventMap.size());

// and logger information is set correctly
assertEquals(loggerName, loggingEventMap.get(AppLoggingUtils.LOGGER_NAME));
assertEquals(fqnOfLogger, loggingEventMap.get(AppLoggingUtils.LOGGER_FQCN));
assertEquals(loggerName, loggingEventMap.get(LOGGER_NAME));
assertEquals(fqnOfLogger, loggingEventMap.get(LOGGER_FQCN));

// and message and timestamp are set correctly
assertEquals(message, loggingEventMap.get(AppLoggingUtils.MESSAGE));
assertNotNull(loggingEventMap.get(AppLoggingUtils.TIMESTAMP));
assertEquals(message, loggingEventMap.get(MESSAGE));
assertNotNull(loggingEventMap.get(TIMESTAMP));

// and level is set correctly
if (level != null) {
assertEquals(level.toString(), loggingEventMap.get(AppLoggingUtils.LEVEL));
assertEquals(level.toString(), loggingEventMap.get(LEVEL));
} else {
assertNull(loggingEventMap.get(AppLoggingUtils.LEVEL));
assertNull(loggingEventMap.get(LEVEL));
}

// and thread fields are not null
assertEquals(event.getThreadName(), loggingEventMap.get(AppLoggingUtils.THREAD_NAME));
assertNotNull(loggingEventMap.get(AppLoggingUtils.THREAD_ID));
assertEquals(event.getThreadName(), loggingEventMap.get(THREAD_NAME));
assertNotNull(loggingEventMap.get(THREAD_ID));

// and error information is set correctly
if (t != null) {
assertEquals(t.getMessage(), loggingEventMap.get(AppLoggingUtils.ERROR_MESSAGE));
assertNotNull(loggingEventMap.get(AppLoggingUtils.ERROR_STACK));
assertEquals(t.getClass().getCanonicalName(), loggingEventMap.get(AppLoggingUtils.ERROR_CLASS));
assertEquals(t.getMessage(), loggingEventMap.get(ERROR_MESSAGE));
assertNotNull(loggingEventMap.get(ERROR_STACK));
assertEquals(t.getClass().getCanonicalName(), loggingEventMap.get(ERROR_CLASS));
} else {
assertNull(loggingEventMap.get(AppLoggingUtils.ERROR_MESSAGE));
assertNull(loggingEventMap.get(AppLoggingUtils.ERROR_STACK));
assertNull(loggingEventMap.get(AppLoggingUtils.ERROR_CLASS));
assertNull(loggingEventMap.get(ERROR_MESSAGE));
assertNull(loggingEventMap.get(ERROR_STACK));
assertNull(loggingEventMap.get(ERROR_CLASS));
}

// and MDC property should be added if appropriate
Expand All @@ -118,5 +134,12 @@ void testLoggingEventCreation(
} else {
assertNull(loggingEventMap.get(new LogAttributeKey("some", LogAttributeType.CONTEXT)));
}

// and tags should be added if present
if (!tags.isEmpty()) {
assertEquals("tagValue", loggingEventMap.get(new LogAttributeKey("tagKey", LogAttributeType.TAG)));
} else {
assertNull(loggingEventMap.get(new LogAttributeKey("tagKey", LogAttributeType.TAG)));
}
}
}
Loading