Skip to content

Commit

Permalink
Merge pull request #1126 from newrelic/NR-59596-jboss-logging
Browse files Browse the repository at this point in the history
Nr 59596 jboss logging
  • Loading branch information
jasonjkeller authored Jan 23, 2023
2 parents 1d94787 + f0d8fa9 commit 05be5e4
Show file tree
Hide file tree
Showing 13 changed files with 362 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
import java.util.Map;

public class AppLoggingUtils {
public static final int DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES = 10;
public static final int DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES = 11;
// Log message attributes
public static final LogAttributeKey INSTRUMENTATION = new LogAttributeKey("instrumentation", LogAttributeType.AGENT);
public static final LogAttributeKey MESSAGE = new LogAttributeKey("message", LogAttributeType.AGENT);
public static final LogAttributeKey TIMESTAMP = new LogAttributeKey("timestamp", LogAttributeType.AGENT);
public static final LogAttributeKey LEVEL = new LogAttributeKey("level", LogAttributeType.AGENT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
import java.util.HashMap;
import java.util.Map;

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);
logEventMap.put(INSTRUMENTATION, "apache-log4j-1");
addLoggerInfo(event, logEventMap);
addMessageAndTs(event, logEventMap);
addLevel(event, logEventMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ public class LoggingEventMapTest {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
Expand All @@ -46,6 +47,7 @@ public static void recordNewRelicLogEvent(LogEvent event) {
if (shouldCreateLogEvent(message, throwable)) {
Map<String, String> contextData = event.getContextMap();
Map<LogAttributeKey, Object> logEventMap = new HashMap<>(calculateInitialMapSize(contextData));
logEventMap.put(INSTRUMENTATION, "apache-log4j-2");
if (message != null) {
String formattedMessage = message.getFormattedMessage();
if (formattedMessage != null && !formattedMessage.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand All @@ -43,6 +44,7 @@ public static void recordNewRelicLogEvent(LogRecord record) {
if (shouldCreateLogEvent(message, throwable)) {
// JUL does not directly support MDC, so we only initialize the map size based on standard attributes
Map<LogAttributeKey, Object> logEventMap = new HashMap<>(DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES);
logEventMap.put(INSTRUMENTATION, "java.logging-jdk8");
logEventMap.put(MESSAGE, message);
logEventMap.put(TIMESTAMP, record.getMillis());

Expand Down
23 changes: 23 additions & 0 deletions instrumentation/jboss-logging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# JBoss Logging

JBoss Logging is a "logging bridge" that integrates with various logging frameworks.

JBoss Logging bridges to the following backends:
- JBoss LogManager (usually only used with WildFly app server)
- Log4j 2
- Log4j 1
- Slf4j
- JDK logging (aka JUL)

This means that when using the `org.jboss.logging:jboss-logging` API, this instrumentation will only apply if the
app is also configured to use the JBoss Logging subsystem (default in JBoss/Wildfly servers) which utilizes the JBoss LogManager.
If the backend is any other logging framework (e.g. log4j) then the instrumentation for that framework will apply instead.

This instrumentation weaves `org.jboss.logmanager.Logger` (which extends `java.util.logging.Logger`)
to generate logging metrics and forward log events. However, this instrumentation does not do local log
decorating as that functionality is already handled by the JUL instrumentation module.

## Testing

Because this instrumentation requires a fully configured JBoss logging subsystem it is not feasible to test it in instrumentation tests.
Instead, the tests can be found in the logging.py AIT which stands up a Wildfly server.
23 changes: 23 additions & 0 deletions instrumentation/jboss-logging/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
dependencies {
implementation(project(":agent-bridge"))
implementation 'org.jboss.logmanager:jboss-logmanager:2.1.19.Final'
}

jar {
manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.jboss.logging' }
}

verifyInstrumentation {
passesOnly('org.jboss.logmanager:jboss-logmanager:[1.3.0.Final,)')
}

site {
title 'JBoss Logging'
type 'Other'
versionOverride '[1.3.0,)'
}

compileJava {
options.fork = true
options.bootstrapClasspath = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
*
* * Copyright 2023 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.instrumentation.jboss;

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.jboss.logmanager.ExtLogRecord;

import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

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 record to parse
*/
public static void recordNewRelicLogEvent(ExtLogRecord record) {
if (record != null) {
String message = record.getMessage();
Throwable throwable = record.getThrown();

if (shouldCreateLogEvent(message, throwable)) {
Map<String, String> mdcCopy = record.getMdcCopy();
Map<LogAttributeKey, Object> logEventMap = new HashMap<>(calculateInitialMapSize(mdcCopy));
logEventMap.put(INSTRUMENTATION, "jboss.logging");
logEventMap.put(MESSAGE, message);
logEventMap.put(TIMESTAMP, record.getMillis());

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

Level level = record.getLevel();
if (level != null) {
String levelName = level.getName();
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 = Thread.currentThread().getName();
if (threadName != null) {
logEventMap.put(THREAD_NAME, threadName);
}

logEventMap.put(THREAD_ID, record.getThreadID());

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

String loggerFqcn = record.getSourceClassName();
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(String message, Throwable throwable) {
return (message != null) || !ExceptionUtil.isThrowableNull(throwable);
}

private static int calculateInitialMapSize(Map<String, String> mdcPropertyMap) {
return AppLoggingUtils.isAppLoggingContextDataEnabled() && mdcPropertyMap != null
? mdcPropertyMap.size() + DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES
: DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
*
* * Copyright 2023 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.instrumentation.jboss;

public class ExceptionUtil {
public static final int MAX_STACK_SIZE = 300;

public static boolean isThrowableNull(Throwable throwable) {
return throwable == null;
}

public static String getErrorStack(Throwable throwable) {
if (isThrowableNull(throwable)) {
return null;
}

StackTraceElement[] stack = throwable.getStackTrace();
return getErrorStack(stack);
}

public static String getErrorStack(StackTraceElement[] stack) {
return getErrorStack(stack, MAX_STACK_SIZE);
}

public static String getErrorStack(StackTraceElement[] stack, Integer maxStackSize) {
if (stack == null || stack.length == 0) {
return null;
}

StringBuilder stackBuilder = new StringBuilder();
int stackSizeLimit = Math.min(maxStackSize, stack.length);
for (int i = 0; i < stackSizeLimit; i++) {
stackBuilder.append(" at ").append(stack[i].toString()).append("\n");
}
return stackBuilder.toString();
}

public static String getErrorMessage(Throwable throwable) {
if (isThrowableNull(throwable)) {
return null;
}
return throwable.getMessage();
}

public static String getErrorClass(Throwable throwable) {
if (isThrowableNull(throwable)) {
return null;
}
return throwable.getClass().getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
*
* * Copyright 2023 New Relic Corporation. All rights reserved.
* * SPDX-License-Identifier: Apache-2.0
*
*/

package org.jboss.logmanager;

import com.newrelic.api.agent.NewRelic;
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 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.instrumentation.jboss.AgentUtil.recordNewRelicLogEvent;

@Weave(originalName = "org.jboss.logmanager.Logger")
public class Logger_Instrumentation {

@NewField
public static AtomicBoolean instrumented = new AtomicBoolean(false);

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

public void logRaw(final ExtLogRecord record) {
Weaver.callOriginal();

if (isApplicationLoggingEnabled()) {
if (isApplicationLoggingMetricsEnabled()) {
// Generate log level metrics
NewRelic.incrementCounter("Logging/lines");
NewRelic.incrementCounter("Logging/lines/" + record.getLevel().toString());
}
if (isApplicationLoggingForwardingEnabled()) {
// Record and send LogEvent to New Relic
recordNewRelicLogEvent(record);
}
}
}
}
Loading

0 comments on commit 05be5e4

Please sign in to comment.