diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/Agent.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/Agent.java index b3af214510..acab69947a 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/Agent.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/Agent.java @@ -7,6 +7,7 @@ package com.newrelic.agent.bridge; +import com.newrelic.api.agent.Logs; import com.newrelic.api.agent.NewRelic; /** @@ -87,4 +88,11 @@ public interface Agent extends com.newrelic.api.agent.Agent { */ boolean ignoreIfUnstartedAsyncContext(Object activityContext); + /** + * Provides access to the LogSender events API. + * + * @return Object used to add custom events. + */ + Logs getLogSender(); + } diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java index 8b5d48b74f..68b610559d 100644 --- a/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpAgent.java @@ -10,6 +10,7 @@ import com.newrelic.api.agent.Config; import com.newrelic.api.agent.Insights; import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.Logs; import com.newrelic.api.agent.MetricAggregator; import com.newrelic.api.agent.TraceMetadata; @@ -63,6 +64,11 @@ public Insights getInsights() { return NoOpInsights.INSTANCE; } + @Override + public Logs getLogSender() { + return NoOpLogs.INSTANCE; + } + @Override public boolean startAsyncActivity(Object activityContext) { return false; diff --git a/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpLogs.java b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpLogs.java new file mode 100644 index 0000000000..7825027abc --- /dev/null +++ b/agent-bridge/src/main/java/com/newrelic/agent/bridge/NoOpLogs.java @@ -0,0 +1,24 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.bridge; + +import com.newrelic.api.agent.Logs; + +import java.util.Map; + +class NoOpLogs implements Logs { + static final Logs INSTANCE = new NoOpLogs(); + + private NoOpLogs() { + } + + @Override + public void recordLogEvent(Map attributes) { + } + +} diff --git a/agent-bridge/src/main/java/com/newrelic/api/agent/Logs.java b/agent-bridge/src/main/java/com/newrelic/api/agent/Logs.java new file mode 100644 index 0000000000..bd8170e9c5 --- /dev/null +++ b/agent-bridge/src/main/java/com/newrelic/api/agent/Logs.java @@ -0,0 +1,26 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.api.agent; + +import java.util.Map; + +/** + * Used to send LogEvents to New Relic. Each LogEvent represents a single log line. + */ +public interface Logs { + + /** + * Sends a LogEvent for the current application. + * + * @param attributes A map of log event data (e.g. log message, log timestamp, log level) + * Each key should be a String and each value should be a String, Number, or Boolean. + * For map values that are not String, Number, or Boolean object types the toString value will be used. + * @since 7.6.0 + */ + void recordLogEvent(Map attributes); +} diff --git a/agent-model/src/main/java/com/newrelic/agent/model/LogEvent.java b/agent-model/src/main/java/com/newrelic/agent/model/LogEvent.java new file mode 100644 index 0000000000..ded1bfce80 --- /dev/null +++ b/agent-model/src/main/java/com/newrelic/agent/model/LogEvent.java @@ -0,0 +1,43 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.model; + +import org.json.simple.JSONObject; +import org.json.simple.JSONStreamAware; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; + +public class LogEvent extends AnalyticsEvent implements JSONStreamAware { + + public static final String LOG_EVENT_TYPE = "LogEvent"; + + private volatile float mutablePriority; + + public LogEvent(Map attributes, float priority) { + super(LOG_EVENT_TYPE, System.currentTimeMillis(), priority, attributes); + this.mutablePriority = priority; + } + + @Override + public float getPriority() { + return mutablePriority; + } + + public void setPriority(float priority) { + this.mutablePriority = priority; + } + + @SuppressWarnings("unchecked") + @Override + public void writeJSONString(Writer out) throws IOException { + JSONObject.writeJSONString(getMutableUserAttributes(), out); + } + +} diff --git a/agent-model/src/test/java/com/newrelic/agent/model/LogEventTest.java b/agent-model/src/test/java/com/newrelic/agent/model/LogEventTest.java new file mode 100644 index 0000000000..238fdac1e5 --- /dev/null +++ b/agent-model/src/test/java/com/newrelic/agent/model/LogEventTest.java @@ -0,0 +1,47 @@ +package com.newrelic.agent.model; + +import org.junit.Test; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class LogEventTest { + + @Test + public void testConstructor() { + LogEvent logEvent = new LogEvent(Collections.emptyMap(), 0); + + assertEquals(logEvent.getPriority(), 0, 0); + assertNotNull(logEvent.getMutableUserAttributes()); + assertTrue(logEvent.getMutableUserAttributes().isEmpty()); + } + + @Test + public void testPriorityAccessors() { + LogEvent logEvent = new LogEvent(Collections.emptyMap(), 0); + + assertEquals(0, logEvent.getPriority(), 0); + + logEvent.setPriority(1); + + assertEquals(1, logEvent.getPriority(),0); + } + + @Test + public void testJsonString() throws IOException { + Map attributes = new HashMap<>(); + attributes.put("key", "value"); + + LogEvent logEvent = new LogEvent(attributes, 0); + StringWriter writer = new StringWriter(); + + logEvent.writeJSONString(writer); + + assertEquals("{\"key\":\"value\"}", writer.toString()); + } +} \ No newline at end of file diff --git a/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java b/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java index 35a5f7b040..8e26b041e3 100644 --- a/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java +++ b/functional_test/src/test/java/com/newrelic/agent/extension/FakeExtensionAgent.java @@ -13,6 +13,7 @@ import com.newrelic.api.agent.Config; import com.newrelic.api.agent.Insights; import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.Logs; import com.newrelic.api.agent.MetricAggregator; import com.newrelic.api.agent.TraceMetadata; @@ -59,4 +60,9 @@ public Logger getLogger() { @Override public boolean ignoreIfUnstartedAsyncContext(Object activityContext) { throw new RuntimeException(); } + + @Override + public Logs getLogSender() { + throw new RuntimeException(); + } } diff --git a/functional_test/src/test/java/test/newrelic/test/agent/JavaUtilLoggerTest.java b/functional_test/src/test/java/test/newrelic/test/agent/JavaUtilLoggerTest.java new file mode 100644 index 0000000000..0d6c6821b8 --- /dev/null +++ b/functional_test/src/test/java/test/newrelic/test/agent/JavaUtilLoggerTest.java @@ -0,0 +1,139 @@ +package test.newrelic.test.agent; + +import com.newrelic.agent.Transaction; +import com.newrelic.agent.stats.SimpleStatsEngine; +import com.newrelic.agent.stats.TransactionStats; +import com.newrelic.api.agent.Trace; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * Test for com.newrelic.instrumentation.java.logging-jdk8 instrumentation + */ +public class JavaUtilLoggerTest { + + private static final String CAPTURED = "This log message should be captured"; + private static final String NOT_CAPTURED = "This message should NOT be captured"; + + @Before + public void setup() { + Transaction.clearTransaction(); + } + + @Trace(dispatcher = true) + @Test + public void shouldIncrementEmittedLogsCountersIndependentlyIfLogLevelEnabled() { + // Given + final Logger logger = Logger.getLogger(JavaUtilLoggerTest.class.getName()); + logger.setLevel(Level.INFO); + + // When + logger.finest(NOT_CAPTURED); + logger.finer(NOT_CAPTURED); + logger.fine(NOT_CAPTURED); + logger.config(NOT_CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.warning(CAPTURED); + logger.warning(CAPTURED); + logger.warning(CAPTURED); + logger.warning(CAPTURED); + logger.severe(CAPTURED); + + // Then + Map metrics = getLogMetricsCounts(); + Assert.assertEquals(8, (int) metrics.get("Logging/lines")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/FINEST")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/FINER")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/FINE")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/CONFIG")); + Assert.assertEquals(3, (int) metrics.get("Logging/lines/INFO")); + Assert.assertEquals(4, (int) metrics.get("Logging/lines/WARNING")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/SEVERE")); + } + + @Trace(dispatcher = true) + @Test + public void shouldIncrementAllEmittedLogCountersIfLogLevelIsSetToFinest() { + // Given + final Logger logger = Logger.getLogger(JavaUtilLoggerTest.class.getName()); + logger.setLevel(Level.FINEST); + + // When + logger.finest(CAPTURED); + logger.finer(CAPTURED); + logger.fine(CAPTURED); + logger.config(CAPTURED); + logger.info(CAPTURED); + logger.warning(CAPTURED); + logger.severe(CAPTURED); + + // Then + Map metrics = getLogMetricsCounts(); + Assert.assertEquals(7, (int) metrics.get("Logging/lines")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/FINEST")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/FINER")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/FINE")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/CONFIG")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/INFO")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/WARNING")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/SEVERE")); + } + + @Trace(dispatcher = true) + @Test + public void shouldIncrementEmittedLogsCountersIndependentlyIfLogLevelEnabledEvenLoggingLogRecordsDirectly() { + // Given + final Logger logger = Logger.getLogger(JavaUtilLoggerTest.class.getName()); + logger.setLevel(Level.INFO); + + // When + logger.log(new LogRecord(Level.FINEST, NOT_CAPTURED)); + logger.log(new LogRecord(Level.FINER, NOT_CAPTURED)); + logger.log(new LogRecord(Level.FINE, NOT_CAPTURED)); + logger.log(new LogRecord(Level.CONFIG, NOT_CAPTURED)); + logger.log(new LogRecord(Level.INFO, CAPTURED)); + logger.log(new LogRecord(Level.INFO, CAPTURED)); + logger.log(new LogRecord(Level.INFO, CAPTURED)); + logger.log(new LogRecord(Level.WARNING, CAPTURED)); + logger.log(new LogRecord(Level.WARNING, CAPTURED)); + logger.log(new LogRecord(Level.WARNING, CAPTURED)); + logger.log(new LogRecord(Level.WARNING, CAPTURED)); + logger.log(new LogRecord(Level.SEVERE, CAPTURED)); + + // Then + Map metrics = getLogMetricsCounts(); + Assert.assertEquals(8, (int) metrics.get("Logging/lines")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/FINEST")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/FINER")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/FINE")); + Assert.assertEquals(0, (int) metrics.get("Logging/lines/CONFIG")); + Assert.assertEquals(3, (int) metrics.get("Logging/lines/INFO")); + Assert.assertEquals(4, (int) metrics.get("Logging/lines/WARNING")); + Assert.assertEquals(1, (int) metrics.get("Logging/lines/SEVERE")); + } + + private Map getLogMetricsCounts() { + Transaction transaction = Transaction.getTransaction(); + TransactionStats transactionStats = transaction.getTransactionActivity().getTransactionStats(); + SimpleStatsEngine engine = transactionStats.getUnscopedStats(); + final Map metrics = new HashMap<>(); + metrics.put("Logging/lines", engine.getStats("Logging/lines").getCallCount()); + metrics.put("Logging/lines/FINEST", engine.getStats("Logging/lines/FINEST").getCallCount()); + metrics.put("Logging/lines/FINER", engine.getStats("Logging/lines/FINER").getCallCount()); + metrics.put("Logging/lines/FINE", engine.getStats("Logging/lines/FINE").getCallCount()); + metrics.put("Logging/lines/CONFIG", engine.getStats("Logging/lines/CONFIG").getCallCount()); + metrics.put("Logging/lines/INFO", engine.getStats("Logging/lines/INFO").getCallCount()); + metrics.put("Logging/lines/WARNING", engine.getStats("Logging/lines/WARNING").getCallCount()); + metrics.put("Logging/lines/SEVERE", engine.getStats("Logging/lines/SEVERE").getCallCount()); + return metrics; + } +} diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/Introspector.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/Introspector.java index 7d117ae669..8e6e928444 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/Introspector.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/Introspector.java @@ -7,6 +7,8 @@ package com.newrelic.agent.introspec; +import com.newrelic.agent.model.LogEvent; + import java.util.Collection; import java.util.Map; @@ -159,10 +161,30 @@ public interface Introspector { */ String getDispatcherVersion(); + /** + * Returns all span events that were collected since this was initialized or cleared. + * + * @return collection of SpanEvents or null if there are none + */ Collection getSpanEvents(); + /** + * Clear all existing SpanEvents + */ void clearSpanEvents(); + /** + * Returns all log events that were collected since this was initialized or cleared. + * + * @return collection of LogEvents or null if there are none + */ + Collection getLogEvents(); + + /** + * Clear all existing LogEvents + */ + void clearLogEvents(); + /** * Return random port available * diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorImpl.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorImpl.java index e058781a55..6d53183706 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorImpl.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorImpl.java @@ -22,6 +22,7 @@ import com.newrelic.agent.introspec.SpanEvent; import com.newrelic.agent.introspec.TracedMetricData; import com.newrelic.agent.introspec.TransactionTrace; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.stats.TransactionStats; @@ -228,6 +229,18 @@ public void clearSpanEvents() { service.clearReservoir(); } + @Override + public Collection getLogEvents() { + IntrospectorLogSenderService service = (IntrospectorLogSenderService) ServiceFactory.getServiceManager().getLogSenderService(); + return service.getLogEvents(); + } + + @Override + public void clearLogEvents() { + IntrospectorLogSenderService service = (IntrospectorLogSenderService) ServiceFactory.getServiceManager().getLogSenderService(); + service.clearReservoir(); + } + @Override public int getRandomPort() { int port; diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorLogSenderService.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorLogSenderService.java new file mode 100644 index 0000000000..35392fccc1 --- /dev/null +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorLogSenderService.java @@ -0,0 +1,140 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.introspec.internal; + +import com.newrelic.agent.Agent; +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.deps.com.google.common.collect.LinkedListMultimap; +import com.newrelic.agent.deps.com.google.common.collect.ListMultimap; +import com.newrelic.agent.deps.com.google.common.collect.Maps; +import com.newrelic.agent.deps.com.google.common.collect.Multimaps; +import com.newrelic.agent.logging.IAgentLogger; +import com.newrelic.agent.model.AnalyticsEvent; +import com.newrelic.agent.model.LogEvent; +import com.newrelic.agent.service.logging.LogSenderService; +import com.newrelic.agent.tracing.DistributedTraceServiceImpl; +import com.newrelic.api.agent.Logs; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import static com.newrelic.agent.model.LogEvent.LOG_EVENT_TYPE; + +class IntrospectorLogSenderService implements LogSenderService { + + private static String SERVICE_NAME = "LogSenderService"; + private ListMultimap events = Multimaps.synchronizedListMultimap(LinkedListMultimap.create()); + + @Override + public String getName() { + return SERVICE_NAME; + } + + @Override + public void start() throws Exception { + } + + @Override + public void stop() throws Exception { + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public IAgentLogger getLogger() { + return Agent.LOG; + } + + @Override + public boolean isStarted() { + return true; + } + + @Override + public boolean isStopped() { + return false; + } + + @Override + public boolean isStartedOrStarting() { + return true; + } + + @Override + public boolean isStoppedOrStopping() { + return false; + } + + @Override + public void recordLogEvent(Map attributes) { + if (AnalyticsEvent.isValidType(LOG_EVENT_TYPE)) { + Map atts = Maps.newHashMap(attributes); + LogEvent event = new LogEvent(atts, DistributedTraceServiceImpl.nextTruncatedFloat()); + storeEvent("TestApp", event); + } + } + + @Override + public Logs getTransactionLogs(AgentConfig config) { + return this; + } + + @Override + public void storeEvent(String appName, LogEvent event) { + events.put(event.getType(), event); + } + + @Override + public void addHarvestableToService(String s) { + } + + public Collection getLogEvents() { + return Collections.unmodifiableCollection(events.values()); + } + + public void clear() { + events.clear(); + } + + @Override + public void harvestEvents(String appName) { + } + + @Override + public String getEventHarvestIntervalMetric() { + return ""; + } + + @Override + public String getReportPeriodInSecondsMetric() { + return ""; + } + + @Override + public String getEventHarvestLimitMetric() { + return ""; + } + + @Override + public int getMaxSamplesStored() { + return 0; + } + + @Override + public void setMaxSamplesStored(int maxSamplesStored) { + } + + @Override + public void clearReservoir() { + events.clear(); + } +} diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java index 3fbacdada5..b0cc1659ac 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorRPMService.java @@ -12,6 +12,7 @@ import com.newrelic.agent.errors.TracedError; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.profile.ProfileData; import com.newrelic.agent.service.AbstractService; @@ -149,6 +150,10 @@ public void sendAnalyticsEvents(int reservoirSize, int eventsSeen, Collection events) { } + @Override + public void sendLogEvents(Collection events) { + } + @Override public void sendErrorEvents(int reservoirSize, int eventsSeen, Collection events) { } diff --git a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorServiceManager.java b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorServiceManager.java index e9fc9b427e..3fb9b4d427 100644 --- a/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorServiceManager.java +++ b/instrumentation-test/src/main/java/com/newrelic/agent/introspec/internal/IntrospectorServiceManager.java @@ -40,6 +40,7 @@ import com.newrelic.agent.service.ServiceManager; import com.newrelic.agent.service.analytics.*; import com.newrelic.agent.service.async.AsyncTransactionService; +import com.newrelic.agent.service.logging.LogSenderService; import com.newrelic.agent.service.module.JarCollectorService; import com.newrelic.agent.sql.SqlTraceService; import com.newrelic.agent.sql.SqlTraceServiceImpl; @@ -80,6 +81,7 @@ class IntrospectorServiceManager extends AbstractService implements ServiceManag private volatile AsyncTransactionService asyncTxService; private volatile CircuitBreakerService circuitBreakerService; private volatile InsightsService insightsService; + private volatile LogSenderService logSenderService; private volatile DistributedTraceServiceImpl distributedTraceService; private volatile SpanEventsService spanEventsService; private volatile SourceLanguageService sourceLanguageService; @@ -141,6 +143,7 @@ private void setup(Map config) { harvestService = new IntrospectorHarvestService(); sqlTraceService = new SqlTraceServiceImpl(); insightsService = new IntrospectorInsightsService(); + logSenderService = new IntrospectorLogSenderService(); expirationService = new ExpirationService(); dbService = new DatabaseService(); jarCollectorService = new IgnoringJarCollectorService(); @@ -389,6 +392,11 @@ public InsightsService getInsights() { return insightsService; } + @Override + public LogSenderService getLogSenderService() { + return logSenderService; + } + @Override public CircuitBreakerService getCircuitBreakerService() { return circuitBreakerService; diff --git a/instrumentation/apache-log4j-1/build.gradle b/instrumentation/apache-log4j-1/build.gradle new file mode 100644 index 0000000000..c9dadae145 --- /dev/null +++ b/instrumentation/apache-log4j-1/build.gradle @@ -0,0 +1,17 @@ +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.apache-log4j-1' } +} + +dependencies { + implementation(project(":agent-bridge")) + implementation("log4j:log4j:1.2.17") +} + +verifyInstrumentation { + passesOnly("log4j:log4j:[1.1.3,)") +} + +site { + title 'Log4j-1' + type 'Framework' +} diff --git a/instrumentation/apache-log4j-1/src/main/java/com/nr/agent/instrumentation/log4j1/AgentUtil.java b/instrumentation/apache-log4j-1/src/main/java/com/nr/agent/instrumentation/log4j1/AgentUtil.java new file mode 100644 index 0000000000..9fd8cfa8f6 --- /dev/null +++ b/instrumentation/apache-log4j-1/src/main/java/com/nr/agent/instrumentation/log4j1/AgentUtil.java @@ -0,0 +1,117 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.log4j1; + +import com.newrelic.api.agent.NewRelic; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.logging.Level; + +public class AgentUtil { + public static final int DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES = 3; + // Log message attributes + public static final String MESSAGE = "message"; + public static final String TIMESTAMP = "timestamp"; + public static final String LOG_LEVEL = "log.level"; + public static final String UNKNOWN = "UNKNOWN"; + // Linking metadata attributes used in blob + private static final String BLOB_PREFIX = "NR-LINKING"; + private static final String BLOB_DELIMITER = "|"; + private static final String TRACE_ID = "trace.id"; + private static final String HOSTNAME = "hostname"; + private static final String ENTITY_GUID = "entity.guid"; + private static final String ENTITY_NAME = "entity.name"; + private static final String SPAN_ID = "span.id"; + // Enabled defaults + private static final boolean APP_LOGGING_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_METRICS_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_FORWARDING_DEFAULT_ENABLED = false; + private static final boolean APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED = false; + + /** + * Gets a String representing the agent linking metadata in blob format: + * NR-LINKING|entity.guid|hostname|trace.id|span.id|entity.name| + * + * @return agent linking metadata string blob + */ + public static String getLinkingMetadataBlob() { + Map agentLinkingMetadata = NewRelic.getAgent().getLinkingMetadata(); + StringBuilder blob = new StringBuilder(); + blob.append(" ").append(BLOB_PREFIX).append(BLOB_DELIMITER); + + if (agentLinkingMetadata != null && agentLinkingMetadata.size() > 0) { + appendAttributeToBlob(agentLinkingMetadata.get(ENTITY_GUID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(HOSTNAME), blob); + appendAttributeToBlob(agentLinkingMetadata.get(TRACE_ID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(SPAN_ID), blob); + appendAttributeToBlob(urlEncode(agentLinkingMetadata.get(ENTITY_NAME)), blob); + } + return blob.toString(); + } + + private static void appendAttributeToBlob(String attribute, StringBuilder blob) { + if (attribute != null && !attribute.isEmpty()) { + blob.append(attribute); + } + blob.append(BLOB_DELIMITER); + } + + /** + * URL encode a String value. + * + * @param value String to encode + * @return URL encoded String + */ + static String urlEncode(String value) { + try { + value = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + NewRelic.getAgent().getLogger().log(Level.WARNING, "Unable to URL encode entity.name for application_logging.local_decorating", e); + } + return value; + } + + /** + * Check if all application_logging features are enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.enabled", APP_LOGGING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging metrics feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingMetricsEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.metrics.enabled", APP_LOGGING_METRICS_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging forwarding feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingForwardingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.forwarding.enabled", APP_LOGGING_FORWARDING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging local_decorating feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingLocalDecoratingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.local_decorating.enabled", APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED); + } +} diff --git a/instrumentation/apache-log4j-1/src/main/java/org/apache/log4j/Category_Instrumentation.java b/instrumentation/apache-log4j-1/src/main/java/org/apache/log4j/Category_Instrumentation.java new file mode 100644 index 0000000000..ddc1e62802 --- /dev/null +++ b/instrumentation/apache-log4j-1/src/main/java/org/apache/log4j/Category_Instrumentation.java @@ -0,0 +1,32 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package org.apache.log4j; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import static com.nr.agent.instrumentation.log4j1.AgentUtil.isApplicationLoggingEnabled; +import static com.nr.agent.instrumentation.log4j1.AgentUtil.isApplicationLoggingMetricsEnabled; + +@Weave(originalName = "org.apache.log4j.Category") +public class Category_Instrumentation { + + protected void forcedLog(String fqcn, Priority level, Object message, Throwable t) { + // Do nothing if application_logging.enabled: false + if (isApplicationLoggingEnabled()) { + if (isApplicationLoggingMetricsEnabled()) { + // Generate log level metrics + NewRelic.incrementCounter("Logging/lines"); + NewRelic.incrementCounter("Logging/lines/" + level.toString()); + } + } + Weaver.callOriginal(); + } + +} diff --git a/instrumentation/apache-log4j-1/src/test/java/com/nr/agent/instrumentation/log4j1/AgentUtilTest.java b/instrumentation/apache-log4j-1/src/test/java/com/nr/agent/instrumentation/log4j1/AgentUtilTest.java new file mode 100644 index 0000000000..88a1e9aac7 --- /dev/null +++ b/instrumentation/apache-log4j-1/src/test/java/com/nr/agent/instrumentation/log4j1/AgentUtilTest.java @@ -0,0 +1,23 @@ +package com.nr.agent.instrumentation.log4j1; + + +import org.junit.Assert; +import org.junit.Test; + +public class AgentUtilTest { + + @Test + public void testUrlEncoding() { + final String ENCODED_PIPE = "%7C"; + final String ENCODED_SPACE = "+"; + // The main goal of the encoding is to eliminate | characters from the entity.name as | is used as + // the BLOB_DELIMITER for separating the agent metadata attributes that are appended to log files + final String valueToEncode = "|My Application|"; + final String expectedEncodedValue = ENCODED_PIPE + "My" + ENCODED_SPACE + "Application" + ENCODED_PIPE; + + String encodedValue = AgentUtil.urlEncode(valueToEncode); + + Assert.assertEquals(expectedEncodedValue, encodedValue); + } + +} diff --git a/instrumentation/apache-log4j-1/src/test/java/com/nr/agent/instrumentation/log4j1/Category_InstrumentationTest.java b/instrumentation/apache-log4j-1/src/test/java/com/nr/agent/instrumentation/log4j1/Category_InstrumentationTest.java new file mode 100644 index 0000000000..f1f8afb9dc --- /dev/null +++ b/instrumentation/apache-log4j-1/src/test/java/com/nr/agent/instrumentation/log4j1/Category_InstrumentationTest.java @@ -0,0 +1,67 @@ +package com.nr.agent.instrumentation.log4j1; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.MetricsHelper; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "org.apache.log4j" }, configName = "application_logging_enabled.yml") +public class Category_InstrumentationTest { + + private static final String CAPTURED = "This log message should be captured"; + private static final String NOT_CAPTURED = "This message should NOT be captured"; + + @Test + public void shouldIncrementEmittedLogsCountersIndependentlyIfLogLevelEnabled() { + // Given + final Logger logger = Logger.getLogger(Category_InstrumentationTest.class); + logger.setLevel(Level.INFO); + + // When + logger.trace(NOT_CAPTURED); + logger.debug(NOT_CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(8, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(3, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(4, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } + + @Test + public void shouldIncrementAllEmittedLogCountersIfLogLevelIsSetToTrace() { + // Given + final Logger logger = Logger.getLogger(Category_InstrumentationTest.class); + logger.setLevel(Level.TRACE); + + // When + logger.trace(CAPTURED); + logger.debug(CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(5, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } +} diff --git a/instrumentation/apache-log4j-1/src/test/resources/application_logging_enabled.yml b/instrumentation/apache-log4j-1/src/test/resources/application_logging_enabled.yml new file mode 100644 index 0000000000..583ffd2bba --- /dev/null +++ b/instrumentation/apache-log4j-1/src/test/resources/application_logging_enabled.yml @@ -0,0 +1,10 @@ +common: &default_settings + application_logging: + enabled: true + forwarding: + enabled: true + max_samples_stored: 10000 + metrics: + enabled: true + local_decorating: + enabled: false diff --git a/instrumentation/apache-log4j-2/build.gradle b/instrumentation/apache-log4j-2/build.gradle new file mode 100644 index 0000000000..8257ce831b --- /dev/null +++ b/instrumentation/apache-log4j-2/build.gradle @@ -0,0 +1,18 @@ +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.apache-log4j-2' } +} + +dependencies { + implementation(project(":agent-bridge")) + implementation("org.apache.logging.log4j:log4j-core:2.17.1") +} + +verifyInstrumentation { + passesOnly("org.apache.logging.log4j:log4j-core:[2.0.0,)") + excludeRegex '.*(alpha|beta|rc).*' +} + +site { + title 'Log4j2' + type 'Framework' +} 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 new file mode 100644 index 0000000000..ccc7ebc8a8 --- /dev/null +++ b/instrumentation/apache-log4j-2/src/main/java/com/nr/agent/instrumentation/log4j2/AgentUtil.java @@ -0,0 +1,154 @@ +/* + * + * * 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.api.agent.NewRelic; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.message.Message; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class AgentUtil { + public static final int DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES = 3; + // Log message attributes + public static final String MESSAGE = "message"; + public static final String TIMESTAMP = "timestamp"; + public static final String LOG_LEVEL = "log.level"; + public static final String UNKNOWN = "UNKNOWN"; + // Linking metadata attributes used in blob + private static final String BLOB_PREFIX = "NR-LINKING"; + private static final String BLOB_DELIMITER = "|"; + private static final String TRACE_ID = "trace.id"; + private static final String HOSTNAME = "hostname"; + private static final String ENTITY_GUID = "entity.guid"; + private static final String ENTITY_NAME = "entity.name"; + private static final String SPAN_ID = "span.id"; + // Enabled defaults + private static final boolean APP_LOGGING_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_METRICS_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_FORWARDING_DEFAULT_ENABLED = false; + private static final boolean APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED = false; + + /** + * 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(); + if (message != null) { + String formattedMessage = message.getFormattedMessage(); + // Bail out and don't create a LogEvent if log message is empty + if (formattedMessage != null && !formattedMessage.isEmpty()) { + HashMap logEventMap = new HashMap<>(DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES); + + logEventMap.put(MESSAGE, formattedMessage); + logEventMap.put(TIMESTAMP, event.getTimeMillis()); + + Level level = event.getLevel(); + if (level != null) { + String levelName = level.name(); + if (levelName.isEmpty()) { + logEventMap.put(LOG_LEVEL, UNKNOWN); + } else { + logEventMap.put(LOG_LEVEL, levelName); + } + } + + AgentBridge.getAgent().getLogSender().recordLogEvent(logEventMap); + } + } + } + } + + /** + * Gets a String representing the agent linking metadata in blob format: + * NR-LINKING|entity.guid|hostname|trace.id|span.id|entity.name| + * + * @return agent linking metadata string blob + */ + public static String getLinkingMetadataBlob() { + Map agentLinkingMetadata = NewRelic.getAgent().getLinkingMetadata(); + StringBuilder blob = new StringBuilder(); + blob.append(" ").append(BLOB_PREFIX).append(BLOB_DELIMITER); + + if (agentLinkingMetadata != null && agentLinkingMetadata.size() > 0) { + appendAttributeToBlob(agentLinkingMetadata.get(ENTITY_GUID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(HOSTNAME), blob); + appendAttributeToBlob(agentLinkingMetadata.get(TRACE_ID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(SPAN_ID), blob); + appendAttributeToBlob(urlEncode(agentLinkingMetadata.get(ENTITY_NAME)), blob); + } + return blob.toString(); + } + + private static void appendAttributeToBlob(String attribute, StringBuilder blob) { + if (attribute != null && !attribute.isEmpty()) { + blob.append(attribute); + } + blob.append(BLOB_DELIMITER); + } + + /** + * URL encode a String value. + * + * @param value String to encode + * @return URL encoded String + */ + static String urlEncode(String value) { + try { + value = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + NewRelic.getAgent().getLogger().log(java.util.logging.Level.WARNING, "Unable to URL encode entity.name for application_logging.local_decorating", e); + } + return value; + } + + /** + * Check if all application_logging features are enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.enabled", APP_LOGGING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging metrics feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingMetricsEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.metrics.enabled", APP_LOGGING_METRICS_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging forwarding feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingForwardingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.forwarding.enabled", APP_LOGGING_FORWARDING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging local_decorating feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingLocalDecoratingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.local_decorating.enabled", APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED); + } +} diff --git a/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/config/LoggerConfig_Instrumentation.java b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/config/LoggerConfig_Instrumentation.java new file mode 100644 index 0000000000..4359579be2 --- /dev/null +++ b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/config/LoggerConfig_Instrumentation.java @@ -0,0 +1,64 @@ +/* + * + * * 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.nr.agent.instrumentation.log4j2.AgentUtil.isApplicationLoggingEnabled; +import static com.nr.agent.instrumentation.log4j2.AgentUtil.isApplicationLoggingForwardingEnabled; +import static com.nr.agent.instrumentation.log4j2.AgentUtil.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/enabled"); + } + } + + protected void callAppenders(LogEvent event) { + // Do nothing if application_logging.enabled: false + // Do nothing if logger has parents and isAdditive is set to true to avoid duplicated counters and logs + if (isApplicationLoggingEnabled() && 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(); + } + +} 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 new file mode 100644 index 0000000000..3a5810af76 --- /dev/null +++ b/instrumentation/apache-log4j-2/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder_Instrumentation.java @@ -0,0 +1,40 @@ +/* + * + * * Copyright 2022 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 static com.nr.agent.instrumentation.log4j2.AgentUtil.getLinkingMetadataBlob; +import static com.nr.agent.instrumentation.log4j2.AgentUtil.isApplicationLoggingEnabled; +import static com.nr.agent.instrumentation.log4j2.AgentUtil.isApplicationLoggingLocalDecoratingEnabled; + +@Weave(originalName = "org.apache.logging.log4j.core.layout.StringBuilderEncoder", type = MatchType.BaseClass) +public class StringBuilderEncoder_Instrumentation { + + public void encode(final StringBuilder source, final ByteBufferDestination destination) { + // Do nothing if application_logging.enabled: false + if (isApplicationLoggingEnabled()) { + if (isApplicationLoggingLocalDecoratingEnabled()) { + // Append New Relic linking metadata from agent to log message + appendAgentMetadata(source); + } + } + Weaver.callOriginal(); + } + + private void appendAgentMetadata(StringBuilder source) { + int breakLine = source.toString().lastIndexOf("\n"); + if (breakLine != -1) { + source.replace(breakLine, breakLine + 1, ""); + } + source.append(getLinkingMetadataBlob()).append("\n"); + } + +} \ No newline at end of file diff --git a/instrumentation/apache-log4j-2/src/test/java/com/nr/agent/instrumentation/log4j2/AgentUtilTest.java b/instrumentation/apache-log4j-2/src/test/java/com/nr/agent/instrumentation/log4j2/AgentUtilTest.java new file mode 100644 index 0000000000..4a6a5984ad --- /dev/null +++ b/instrumentation/apache-log4j-2/src/test/java/com/nr/agent/instrumentation/log4j2/AgentUtilTest.java @@ -0,0 +1,22 @@ +package com.nr.agent.instrumentation.log4j2; + +import org.junit.Assert; +import org.junit.Test; + +public class AgentUtilTest { + + @Test + public void testUrlEncoding() { + final String ENCODED_PIPE = "%7C"; + final String ENCODED_SPACE = "+"; + // The main goal of the encoding is to eliminate | characters from the entity.name as | is used as + // the BLOB_DELIMITER for separating the agent metadata attributes that are appended to log files + final String valueToEncode = "|My Application|"; + final String expectedEncodedValue = ENCODED_PIPE + "My" + ENCODED_SPACE + "Application" + ENCODED_PIPE; + + String encodedValue = AgentUtil.urlEncode(valueToEncode); + + Assert.assertEquals(expectedEncodedValue, encodedValue); + } + +} diff --git a/instrumentation/apache-log4j-2/src/test/java/com/nr/agent/instrumentation/log4j2/LoggerConfig_InstrumentationTest.java b/instrumentation/apache-log4j-2/src/test/java/com/nr/agent/instrumentation/log4j2/LoggerConfig_InstrumentationTest.java new file mode 100644 index 0000000000..df15051052 --- /dev/null +++ b/instrumentation/apache-log4j-2/src/test/java/com/nr/agent/instrumentation/log4j2/LoggerConfig_InstrumentationTest.java @@ -0,0 +1,601 @@ +package com.nr.agent.instrumentation.log4j2; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.MetricsHelper; +import com.newrelic.agent.model.LogEvent; +import junit.framework.TestCase; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.core.config.AppenderRef; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "org.apache.logging.log4j.core" }, configName = "application_logging_enabled.yml") +public class LoggerConfig_InstrumentationTest extends TestCase { + + private static final String CAPTURED = "This log message should be captured"; + private static final String NOT_CAPTURED = "This message should NOT be captured"; + private final Introspector introspector = InstrumentationTestRunner.getIntrospector(); + + @Before + public void reset() { + Configurator.reconfigure(); + introspector.clearLogEvents(); + } + + @Test + public void testLogEventsAllLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Log at ALL level + setLoggerLevel(Level.ALL); + + int expectedTotalEventsCapturedAtFatal = 1; + logger.fatal(CAPTURED); + + int expectedTotalEventsCapturedAtError = 2; + logger.error(CAPTURED); + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 3; + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 4; + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 5; + logger.debug(CAPTURED); + logger.debug(CAPTURED); + logger.debug(CAPTURED); + logger.debug(CAPTURED); + logger.debug(CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 6; + logger.trace(CAPTURED); + logger.trace(CAPTURED); + logger.trace(CAPTURED); + logger.trace(CAPTURED); + logger.trace(CAPTURED); + logger.trace(CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtFatal + + expectedTotalEventsCapturedAtError + expectedTotalEventsCapturedAtWarn + + expectedTotalEventsCapturedAtInfo + expectedTotalEventsCapturedAtDebug + + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsOffLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Logging is OFF at all levels + setLoggerLevel(Level.OFF); + + int expectedTotalEventsCapturedAtFatal = 0; + logger.fatal(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtError = 0; + logger.error(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 0; + logger.warn(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 0; + logger.info(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = 0; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsFatalLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Log at FATAL level + setLoggerLevel(Level.FATAL); + + int expectedTotalEventsCapturedAtFatal = 1; + logger.fatal(CAPTURED); + + int expectedTotalEventsCapturedAtError = 0; + logger.error(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 0; + logger.warn(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 0; + logger.info(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtFatal + + expectedTotalEventsCapturedAtError + expectedTotalEventsCapturedAtWarn + + expectedTotalEventsCapturedAtInfo + expectedTotalEventsCapturedAtDebug + + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsErrorLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Log at ERROR level + setLoggerLevel(Level.ERROR); + + int expectedTotalEventsCapturedAtFatal = 1; + logger.fatal(CAPTURED); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 0; + logger.warn(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 0; + logger.info(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtFatal + + expectedTotalEventsCapturedAtError + expectedTotalEventsCapturedAtWarn + + expectedTotalEventsCapturedAtInfo + expectedTotalEventsCapturedAtDebug + + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsWarnLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Log at WARN level + setLoggerLevel(Level.WARN); + + int expectedTotalEventsCapturedAtFatal = 1; + logger.fatal(CAPTURED); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 0; + logger.info(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtFatal + + expectedTotalEventsCapturedAtError + expectedTotalEventsCapturedAtWarn + + expectedTotalEventsCapturedAtInfo + expectedTotalEventsCapturedAtDebug + + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsInfoLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Log at INFO level + setLoggerLevel(Level.INFO); + + int expectedTotalEventsCapturedAtFatal = 1; + logger.fatal(CAPTURED); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 1; + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtFatal + + expectedTotalEventsCapturedAtError + expectedTotalEventsCapturedAtWarn + + expectedTotalEventsCapturedAtInfo + expectedTotalEventsCapturedAtDebug + + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsDebugLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Log at DEBUG level + setLoggerLevel(Level.DEBUG); + + int expectedTotalEventsCapturedAtFatal = 1; + logger.fatal(CAPTURED); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 1; + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 1; + logger.debug(CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtFatal + + expectedTotalEventsCapturedAtError + expectedTotalEventsCapturedAtWarn + + expectedTotalEventsCapturedAtInfo + expectedTotalEventsCapturedAtDebug + + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsTraceLevel() { + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + // Log at TRACE level + setLoggerLevel(Level.TRACE); + + int expectedTotalEventsCapturedAtFatal = 1; + logger.fatal(CAPTURED); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 1; + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 1; + logger.debug(CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 1; + logger.trace(CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtFatal + + expectedTotalEventsCapturedAtError + expectedTotalEventsCapturedAtWarn + + expectedTotalEventsCapturedAtInfo + expectedTotalEventsCapturedAtDebug + + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List fatalLevelLogEvents = getFatalLevelLogEvents(logEvents); + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtFatal, fatalLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + private List getTraceLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.TRACE.toString())) + .collect(Collectors.toList()); + } + + private List getDebugLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.DEBUG.toString())) + .collect(Collectors.toList()); + } + + private List getInfoLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.INFO.toString())) + .collect(Collectors.toList()); + } + + private List getWarnLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.WARN.toString())) + .collect(Collectors.toList()); + } + + private List getErrorLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.ERROR.toString())) + .collect(Collectors.toList()); + } + + private List getFatalLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.FATAL.toString())) + .collect(Collectors.toList()); + } + + @Test + public void shouldIncrementEmittedLogsCountersIndependentlyIfLogLevelEnabled() { + // Given + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + setLoggerLevel(Level.INFO); + + // When + logger.trace(NOT_CAPTURED); + logger.debug(NOT_CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(8, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(3, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(4, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } + + @Test + public void shouldIncrementAllEmittedLogCountersIfLogLevelIsSetToTrace() { + // Given + final Logger logger = LogManager.getLogger(LoggerConfig_InstrumentationTest.class); + setLoggerLevel(Level.TRACE); + + // When + logger.trace(CAPTURED); + logger.debug(CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(5, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } + + @Test + public void shouldIncrementAllEmittedLogCountersOnlyOnceWhenMultipleLoggersAreSet() { + // Given + createLogger("A_SPECIAL_LOGGER", createAppender("ConsoleAppender"), Level.TRACE, true); + final Logger logger = LogManager.getLogger("A_SPECIAL_LOGGER"); + setLoggerLevel(Level.TRACE); + + // When + logger.trace(CAPTURED); + logger.debug(CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(5, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } + + @Test + public void shouldIncrementAllEmittedLogCountersRespectingLevelFromOriginalLogger() { + // Given + createLogger("A_SPECIAL_LOGGER", createAppender("ConsoleAppender"), Level.INFO, true); + final Logger logger = LogManager.getLogger("A_SPECIAL_LOGGER"); + setLoggerLevel(Level.ERROR); + + // When + logger.trace(NOT_CAPTURED); + logger.debug(NOT_CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(3, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } + + private void createLogger(String name, Appender appender, Level level, boolean additivity) { + final LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + final Configuration config = ctx.getConfiguration(); + AppenderRef ref = AppenderRef.createAppenderRef("File", null, null); + AppenderRef[] refs = new AppenderRef[] { ref }; + LoggerConfig loggerConfig = LoggerConfig.createLogger(additivity, level, name, "true", refs, null, config, null); + loggerConfig.addAppender(appender, level, null); + config.addLogger(name, loggerConfig); + } + + private Appender createAppender(String name) { + Layout layout = PatternLayout.newBuilder() + .withPattern(PatternLayout.SIMPLE_CONVERSION_PATTERN) + .build(); + Appender appender = ConsoleAppender.newBuilder() + .setName(name) + .setLayout(layout) + .build(); + appender.start(); + return appender; + } + + private void setLoggerLevel(Level level) { + final LoggerContext context = (LoggerContext) LogManager.getContext(false); + final Configuration configuration = context.getConfiguration(); + final LoggerConfig rootConfig = configuration.getLoggerConfig(LogManager.ROOT_LOGGER_NAME); + rootConfig.setLevel(level); + context.updateLoggers(); + } + +} diff --git a/instrumentation/apache-log4j-2/src/test/resources/application_logging_enabled.yml b/instrumentation/apache-log4j-2/src/test/resources/application_logging_enabled.yml new file mode 100644 index 0000000000..583ffd2bba --- /dev/null +++ b/instrumentation/apache-log4j-2/src/test/resources/application_logging_enabled.yml @@ -0,0 +1,10 @@ +common: &default_settings + application_logging: + enabled: true + forwarding: + enabled: true + max_samples_stored: 10000 + metrics: + enabled: true + local_decorating: + enabled: false diff --git a/instrumentation/java.logging-jdk8/build.gradle b/instrumentation/java.logging-jdk8/build.gradle new file mode 100644 index 0000000000..bd72ac879c --- /dev/null +++ b/instrumentation/java.logging-jdk8/build.gradle @@ -0,0 +1,25 @@ +dependencies { + implementation(project(":agent-bridge")) +} + +// This instrumentation module should not use the bootstrap classpath +compileJava.options.bootstrapClasspath = null + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.java.logging-jdk8' } +} + +verifyInstrumentation { + verifyClasspath = false // We don't want to verify classpath since these are JDK classes +} + +site { + title 'Java Logging' + type 'Other' + versionOverride '[8,)' +} + +compileJava { + options.fork = true + options.bootstrapClasspath = null +} \ No newline at end of file diff --git a/instrumentation/java.logging-jdk8/src/main/java/com/nr/instrumentation/jul/AgentUtil.java b/instrumentation/java.logging-jdk8/src/main/java/com/nr/instrumentation/jul/AgentUtil.java new file mode 100644 index 0000000000..c1da1efbe8 --- /dev/null +++ b/instrumentation/java.logging-jdk8/src/main/java/com/nr/instrumentation/jul/AgentUtil.java @@ -0,0 +1,117 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.instrumentation.jul; + +import com.newrelic.api.agent.NewRelic; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.logging.Level; + +public class AgentUtil { + public static final int DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES = 3; + // Log message attributes + public static final String MESSAGE = "message"; + public static final String TIMESTAMP = "timestamp"; + public static final String LOG_LEVEL = "log.level"; + public static final String UNKNOWN = "UNKNOWN"; + // Linking metadata attributes used in blob + private static final String BLOB_PREFIX = "NR-LINKING"; + private static final String BLOB_DELIMITER = "|"; + private static final String TRACE_ID = "trace.id"; + private static final String HOSTNAME = "hostname"; + private static final String ENTITY_GUID = "entity.guid"; + private static final String ENTITY_NAME = "entity.name"; + private static final String SPAN_ID = "span.id"; + // Enabled defaults + private static final boolean APP_LOGGING_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_METRICS_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_FORWARDING_DEFAULT_ENABLED = false; + private static final boolean APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED = false; + + /** + * Gets a String representing the agent linking metadata in blob format: + * NR-LINKING|entity.guid|hostname|trace.id|span.id|entity.name| + * + * @return agent linking metadata string blob + */ + public static String getLinkingMetadataBlob() { + Map agentLinkingMetadata = NewRelic.getAgent().getLinkingMetadata(); + StringBuilder blob = new StringBuilder(); + blob.append(" ").append(BLOB_PREFIX).append(BLOB_DELIMITER); + + if (agentLinkingMetadata != null && agentLinkingMetadata.size() > 0) { + appendAttributeToBlob(agentLinkingMetadata.get(ENTITY_GUID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(HOSTNAME), blob); + appendAttributeToBlob(agentLinkingMetadata.get(TRACE_ID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(SPAN_ID), blob); + appendAttributeToBlob(urlEncode(agentLinkingMetadata.get(ENTITY_NAME)), blob); + } + return blob.toString(); + } + + private static void appendAttributeToBlob(String attribute, StringBuilder blob) { + if (attribute != null && !attribute.isEmpty()) { + blob.append(attribute); + } + blob.append(BLOB_DELIMITER); + } + + /** + * URL encode a String value. + * + * @param value String to encode + * @return URL encoded String + */ + static String urlEncode(String value) { + try { + value = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + NewRelic.getAgent().getLogger().log(Level.WARNING, "Unable to URL encode entity.name for application_logging.local_decorating", e); + } + return value; + } + + /** + * Check if all application_logging features are enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.enabled", APP_LOGGING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging metrics feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingMetricsEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.metrics.enabled", APP_LOGGING_METRICS_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging forwarding feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingForwardingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.forwarding.enabled", APP_LOGGING_FORWARDING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging local_decorating feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingLocalDecoratingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.local_decorating.enabled", APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED); + } +} diff --git a/instrumentation/java.logging-jdk8/src/main/java/java/util/logging/Logger_Instrumentation.java b/instrumentation/java.logging-jdk8/src/main/java/java/util/logging/Logger_Instrumentation.java new file mode 100644 index 0000000000..bef11fa70f --- /dev/null +++ b/instrumentation/java.logging-jdk8/src/main/java/java/util/logging/Logger_Instrumentation.java @@ -0,0 +1,42 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package java.util.logging; + +import com.newrelic.api.agent.NewRelic; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import static com.nr.instrumentation.jul.AgentUtil.isApplicationLoggingEnabled; +import static com.nr.instrumentation.jul.AgentUtil.isApplicationLoggingMetricsEnabled; + +@Weave(originalName = "java.util.logging.Logger") +public class Logger_Instrumentation { + + public Filter getFilter() { + return Weaver.callOriginal(); + } + + public boolean isLoggable(Level level) { + return Boolean.TRUE.equals(Weaver.callOriginal()); + } + + public void log(LogRecord record) { + // Do nothing if application_logging.enabled: false + if (isApplicationLoggingEnabled()) { + if (isApplicationLoggingMetricsEnabled()) { + if (isLoggable(record.getLevel()) && getFilter() == null || getFilter().isLoggable(record)) { + // Generate log level metrics + NewRelic.incrementCounter("Logging/lines"); + NewRelic.incrementCounter("Logging/lines/" + record.getLevel().toString()); + } + } + } + Weaver.callOriginal(); + } + +} diff --git a/instrumentation/java.logging-jdk8/src/test/java/com/nr/instrumentation/jul/AgentUtilTest.java b/instrumentation/java.logging-jdk8/src/test/java/com/nr/instrumentation/jul/AgentUtilTest.java new file mode 100644 index 0000000000..87dff10ece --- /dev/null +++ b/instrumentation/java.logging-jdk8/src/test/java/com/nr/instrumentation/jul/AgentUtilTest.java @@ -0,0 +1,22 @@ +package com.nr.instrumentation.jul; + +import org.junit.Assert; +import org.junit.Test; + +public class AgentUtilTest { + + @Test + public void testUrlEncoding() { + final String ENCODED_PIPE = "%7C"; + final String ENCODED_SPACE = "+"; + // The main goal of the encoding is to eliminate | characters from the entity.name as | is used as + // the BLOB_DELIMITER for separating the agent metadata attributes that are appended to log files + final String valueToEncode = "|My Application|"; + final String expectedEncodedValue = ENCODED_PIPE + "My" + ENCODED_SPACE + "Application" + ENCODED_PIPE; + + String encodedValue = AgentUtil.urlEncode(valueToEncode); + + Assert.assertEquals(expectedEncodedValue, encodedValue); + } + +} diff --git a/instrumentation/logback-classic-1.2/build.gradle b/instrumentation/logback-classic-1.2/build.gradle new file mode 100644 index 0000000000..dcedf18f1c --- /dev/null +++ b/instrumentation/logback-classic-1.2/build.gradle @@ -0,0 +1,19 @@ +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.logback-classic-1.2' } +} + +dependencies { + implementation(project(":agent-bridge")) + implementation("ch.qos.logback:logback-classic:1.2.6") +} + +verifyInstrumentation { + passesOnly("ch.qos.logback:logback-classic:[0.9.3,)") + excludeRegex '.*(alpha|groovyless).*' + excludeRegex 'ch.qos.logback:logback-classic:0.9.6' +} + +site { + title 'Logback' + type 'Framework' +} \ No newline at end of file diff --git a/instrumentation/logback-classic-1.2/src/main/java/ch/qos/logback/classic/Logger_Instrumentation.java b/instrumentation/logback-classic-1.2/src/main/java/ch/qos/logback/classic/Logger_Instrumentation.java new file mode 100644 index 0000000000..b817526a65 --- /dev/null +++ b/instrumentation/logback-classic-1.2/src/main/java/ch/qos/logback/classic/Logger_Instrumentation.java @@ -0,0 +1,46 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package ch.qos.logback.classic; + +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 com.nr.agent.instrumentation.logbackclassic12.AgentUtil; +import org.slf4j.Marker; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Weave(originalName = "ch.qos.logback.classic.Logger", type = MatchType.ExactClass) +public abstract 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/LogbackClassic1.2/enabled"); + } + } + + private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, final Object[] params, + final Throwable t) { + // Do nothing if application_logging.enabled: false + if (AgentUtil.isApplicationLoggingEnabled()) { + if (AgentUtil.isApplicationLoggingMetricsEnabled()) { + // Generate log level metrics + NewRelic.incrementCounter("Logging/lines"); + NewRelic.incrementCounter("Logging/lines/" + level); + } + } + Weaver.callOriginal(); + } +} diff --git a/instrumentation/logback-classic-1.2/src/main/java/ch/qos/logback/classic/spi/LoggingEvent_Instrumentation.java b/instrumentation/logback-classic-1.2/src/main/java/ch/qos/logback/classic/spi/LoggingEvent_Instrumentation.java new file mode 100644 index 0000000000..15859cac00 --- /dev/null +++ b/instrumentation/logback-classic-1.2/src/main/java/ch/qos/logback/classic/spi/LoggingEvent_Instrumentation.java @@ -0,0 +1,82 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package ch.qos.logback.classic.spi; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import com.newrelic.api.agent.weaver.MatchType; +import com.newrelic.api.agent.weaver.Weave; +import com.newrelic.api.agent.weaver.Weaver; + +import static com.nr.agent.instrumentation.logbackclassic12.AgentUtil.getLinkingMetadataBlob; +import static com.nr.agent.instrumentation.logbackclassic12.AgentUtil.isApplicationLoggingEnabled; +import static com.nr.agent.instrumentation.logbackclassic12.AgentUtil.isApplicationLoggingForwardingEnabled; +import static com.nr.agent.instrumentation.logbackclassic12.AgentUtil.isApplicationLoggingLocalDecoratingEnabled; +import static com.nr.agent.instrumentation.logbackclassic12.AgentUtil.recordNewRelicLogEvent; + +@Weave(originalName = "ch.qos.logback.classic.spi.LoggingEvent", type = MatchType.ExactClass) +public class LoggingEvent_Instrumentation { + + transient String fqnOfLoggerClass; + private String loggerName; + private LoggerContext loggerContext; + private LoggerContextVO loggerContextVO; + private transient Level level; + private String message; + private transient Object[] argumentArray; + private ThrowableProxy throwableProxy; + private long timeStamp; + + public LoggingEvent_Instrumentation(String fqcn, Logger logger, Level level, String message, Throwable throwable, Object[] argArray) { + // Do nothing if application_logging.enabled: false + this.fqnOfLoggerClass = fqcn; + this.loggerName = logger.getName(); + this.loggerContext = logger.getLoggerContext(); + this.loggerContextVO = loggerContext.getLoggerContextRemoteView(); + this.level = level; + + boolean applicationLoggingEnabled = isApplicationLoggingEnabled(); + if (applicationLoggingEnabled && isApplicationLoggingLocalDecoratingEnabled()) { + // Append New Relic linking metadata from agent to log message + this.message = message + getLinkingMetadataBlob(); + } else { + this.message = message; + } + + this.argumentArray = argArray; + + if (throwable == null) { + throwable = extractThrowableAnRearrangeArguments(argArray); + } + + if (throwable != null) { + this.throwableProxy = new ThrowableProxy(throwable); + LoggerContext lc = logger.getLoggerContext(); + if (lc.isPackagingDataEnabled()) { + this.throwableProxy.calculatePackagingData(); + } + } + + timeStamp = System.currentTimeMillis(); + + if (applicationLoggingEnabled && isApplicationLoggingForwardingEnabled()) { + // Record and send LogEvent to New Relic + recordNewRelicLogEvent(getFormattedMessage(), timeStamp, level); + } + } + + public String getFormattedMessage() { + return Weaver.callOriginal(); + } + + private Throwable extractThrowableAnRearrangeArguments(Object[] argArray) { + return Weaver.callOriginal(); + } + +} diff --git a/instrumentation/logback-classic-1.2/src/main/java/com/nr/agent/instrumentation/logbackclassic12/AgentUtil.java b/instrumentation/logback-classic-1.2/src/main/java/com/nr/agent/instrumentation/logbackclassic12/AgentUtil.java new file mode 100644 index 0000000000..4e48bac66c --- /dev/null +++ b/instrumentation/logback-classic-1.2/src/main/java/com/nr/agent/instrumentation/logbackclassic12/AgentUtil.java @@ -0,0 +1,145 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.logbackclassic12; + +import ch.qos.logback.classic.Level; +import com.newrelic.agent.bridge.AgentBridge; +import com.newrelic.api.agent.NewRelic; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class AgentUtil { + public static final int DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES = 3; + // Log message attributes + public static final String MESSAGE = "message"; + public static final String TIMESTAMP = "timestamp"; + public static final String LOG_LEVEL = "log.level"; + public static final String UNKNOWN = "UNKNOWN"; + // Linking metadata attributes used in blob + private static final String BLOB_PREFIX = "NR-LINKING"; + private static final String BLOB_DELIMITER = "|"; + private static final String TRACE_ID = "trace.id"; + private static final String HOSTNAME = "hostname"; + private static final String ENTITY_GUID = "entity.guid"; + private static final String ENTITY_NAME = "entity.name"; + private static final String SPAN_ID = "span.id"; + // Enabled defaults + private static final boolean APP_LOGGING_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_METRICS_DEFAULT_ENABLED = true; + private static final boolean APP_LOGGING_FORWARDING_DEFAULT_ENABLED = false; + private static final boolean APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED = false; + + /** + * Record a LogEvent to be sent to New Relic. + * + * @param message log message + * @param timeStampMillis log timestamp + * @param level log level + */ + public static void recordNewRelicLogEvent(String message, long timeStampMillis, Level level) { + // Bail out and don't create a LogEvent if log message is empty + if (!message.isEmpty()) { + HashMap logEventMap = new HashMap<>(DEFAULT_NUM_OF_LOG_EVENT_ATTRIBUTES); + logEventMap.put(MESSAGE, message); + logEventMap.put(TIMESTAMP, timeStampMillis); + + if (level.toString().isEmpty()) { + logEventMap.put(LOG_LEVEL, UNKNOWN); + } else { + logEventMap.put(LOG_LEVEL, level); + } + + AgentBridge.getAgent().getLogSender().recordLogEvent(logEventMap); + } + } + + /** + * Gets a String representing the agent linking metadata in blob format: + * NR-LINKING|entity.guid|hostname|trace.id|span.id|entity.name| + * + * @return agent linking metadata string blob + */ + public static String getLinkingMetadataBlob() { + Map agentLinkingMetadata = NewRelic.getAgent().getLinkingMetadata(); + StringBuilder blob = new StringBuilder(); + blob.append(" ").append(BLOB_PREFIX).append(BLOB_DELIMITER); + + if (agentLinkingMetadata != null && agentLinkingMetadata.size() > 0) { + appendAttributeToBlob(agentLinkingMetadata.get(ENTITY_GUID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(HOSTNAME), blob); + appendAttributeToBlob(agentLinkingMetadata.get(TRACE_ID), blob); + appendAttributeToBlob(agentLinkingMetadata.get(SPAN_ID), blob); + appendAttributeToBlob(urlEncode(agentLinkingMetadata.get(ENTITY_NAME)), blob); + } + return blob.toString(); + } + + private static void appendAttributeToBlob(String attribute, StringBuilder blob) { + if (attribute != null && !attribute.isEmpty()) { + blob.append(attribute); + } + blob.append(BLOB_DELIMITER); + } + + /** + * URL encode a String value. + * + * @param value String to encode + * @return URL encoded String + */ + static String urlEncode(String value) { + try { + value = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + NewRelic.getAgent().getLogger().log(java.util.logging.Level.WARNING, "Unable to URL encode entity.name for application_logging.local_decorating", e); + } + return value; + } + + /** + * Check if all application_logging features are enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.enabled", APP_LOGGING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging metrics feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingMetricsEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.metrics.enabled", APP_LOGGING_METRICS_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging forwarding feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingForwardingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.forwarding.enabled", APP_LOGGING_FORWARDING_DEFAULT_ENABLED); + } + + /** + * Check if the application_logging local_decorating feature is enabled. + * + * @return true if enabled, else false + */ + public static boolean isApplicationLoggingLocalDecoratingEnabled() { + return NewRelic.getAgent().getConfig().getValue("application_logging.local_decorating.enabled", APP_LOGGING_LOCAL_DECORATING_DEFAULT_ENABLED); + } +} diff --git a/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/AgentUtilTest.java b/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/AgentUtilTest.java new file mode 100644 index 0000000000..31a627f9ad --- /dev/null +++ b/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/AgentUtilTest.java @@ -0,0 +1,22 @@ +package com.nr.agent.instrumentation.logbackclassic12; + +import org.junit.Assert; +import org.junit.Test; + +public class AgentUtilTest { + + @Test + public void testUrlEncoding() { + final String ENCODED_PIPE = "%7C"; + final String ENCODED_SPACE = "+"; + // The main goal of the encoding is to eliminate | characters from the entity.name as | is used as + // the BLOB_DELIMITER for separating the agent metadata attributes that are appended to log files + final String valueToEncode = "|My Application|"; + final String expectedEncodedValue = ENCODED_PIPE + "My" + ENCODED_SPACE + "Application" + ENCODED_PIPE; + + String encodedValue = AgentUtil.urlEncode(valueToEncode); + + Assert.assertEquals(expectedEncodedValue, encodedValue); + } + +} diff --git a/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/Logger_InstrumentationTest.java b/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/Logger_InstrumentationTest.java new file mode 100644 index 0000000000..faa82aa20f --- /dev/null +++ b/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/Logger_InstrumentationTest.java @@ -0,0 +1,68 @@ +package com.nr.agent.instrumentation.logbackclassic12; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.MetricsHelper; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.LoggerFactory; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "ch.qos.logback" }, configName = "application_logging_enabled.yml") +public class Logger_InstrumentationTest { + + private static final String CAPTURED = "This log message should be captured"; + private static final String NOT_CAPTURED = "This message should NOT be captured"; + + @Test + public void shouldIncrementEmittedLogsCountersIndependentlyIfLogLevelEnabled() { + // Given + final Logger logger = (Logger) LoggerFactory.getLogger(Logger_InstrumentationTest.class); + logger.setLevel(Level.INFO); + + // When + logger.trace(NOT_CAPTURED); + logger.debug(NOT_CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(8, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(0, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(3, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(4, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } + + @Test + public void shouldIncrementAllEmittedLogCountersIfLogLevelIsSetToTrace() { + // Given + final Logger logger = (Logger) LoggerFactory.getLogger(Logger_InstrumentationTest.class); + logger.setLevel(Level.TRACE); + + // When + logger.trace(CAPTURED); + logger.debug(CAPTURED); + logger.info(CAPTURED); + logger.warn(CAPTURED); + logger.error(CAPTURED); + + // Then + Assert.assertEquals(5, MetricsHelper.getUnscopedMetricCount("Logging/lines")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/TRACE")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/DEBUG")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/INFO")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/WARN")); + Assert.assertEquals(1, MetricsHelper.getUnscopedMetricCount("Logging/lines/ERROR")); + } +} \ No newline at end of file diff --git a/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/LoggingEvent_InstrumentationTest.java b/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/LoggingEvent_InstrumentationTest.java new file mode 100644 index 0000000000..4586af70a7 --- /dev/null +++ b/instrumentation/logback-classic-1.2/src/test/java/com/nr/agent/instrumentation/logbackclassic12/LoggingEvent_InstrumentationTest.java @@ -0,0 +1,363 @@ +package com.nr.agent.instrumentation.logbackclassic12; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.model.LogEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "ch.qos.logback" }, configName = "application_logging_enabled.yml") +public class LoggingEvent_InstrumentationTest { + private static final String CAPTURED = "This log message should be captured"; + private static final String NOT_CAPTURED = "This message should NOT be captured"; + private final Introspector introspector = InstrumentationTestRunner.getIntrospector(); + + @Before + public void reset() { + introspector.clearLogEvents(); + } + + @Test + public void testLogEventsAllLevel() { + final Logger logger = (Logger) LoggerFactory.getLogger(LoggingEvent_InstrumentationTest.class); + // Log at ALL level + logger.setLevel(Level.ALL); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 2; + logger.warn(CAPTURED); + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 3; + logger.info(CAPTURED); + logger.info(CAPTURED); + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 4; + logger.debug(CAPTURED); + logger.debug(CAPTURED); + logger.debug(CAPTURED); + logger.debug(CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 5; + logger.trace(CAPTURED); + logger.trace(CAPTURED); + logger.trace(CAPTURED); + logger.trace(CAPTURED); + logger.trace(CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtError + + expectedTotalEventsCapturedAtWarn + expectedTotalEventsCapturedAtInfo + + expectedTotalEventsCapturedAtDebug + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsOffLevel() { + final Logger logger = (Logger) LoggerFactory.getLogger(LoggingEvent_InstrumentationTest.class); + // Logging is OFF at all levels + logger.setLevel(Level.OFF); + + int expectedTotalEventsCapturedAtError = 0; + logger.error(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 0; + logger.warn(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 0; + logger.info(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = 0; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsErrorLevel() { + final Logger logger = (Logger) LoggerFactory.getLogger(LoggingEvent_InstrumentationTest.class); + // Log at ERROR level + logger.setLevel(Level.ERROR); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 0; + logger.warn(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 0; + logger.info(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtError + + expectedTotalEventsCapturedAtWarn + expectedTotalEventsCapturedAtInfo + + expectedTotalEventsCapturedAtDebug + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsWarnLevel() { + final Logger logger = (Logger) LoggerFactory.getLogger(LoggingEvent_InstrumentationTest.class); + // Log at WARN level + logger.setLevel(Level.WARN); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 0; + logger.info(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtError + + expectedTotalEventsCapturedAtWarn + expectedTotalEventsCapturedAtInfo + + expectedTotalEventsCapturedAtDebug + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsInfoLevel() { + final Logger logger = (Logger) LoggerFactory.getLogger(LoggingEvent_InstrumentationTest.class); + // Log at INFO level + logger.setLevel(Level.INFO); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 1; + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 0; + logger.debug(NOT_CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtError + + expectedTotalEventsCapturedAtWarn + expectedTotalEventsCapturedAtInfo + + expectedTotalEventsCapturedAtDebug + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsDebugLevel() { + final Logger logger = (Logger) LoggerFactory.getLogger(LoggingEvent_InstrumentationTest.class); + // Log at DEBUG level + logger.setLevel(Level.DEBUG); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 1; + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 1; + logger.debug(CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 0; + logger.trace(NOT_CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtError + + expectedTotalEventsCapturedAtWarn + expectedTotalEventsCapturedAtInfo + + expectedTotalEventsCapturedAtDebug + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + @Test + public void testLogEventsTraceLevel() { + final Logger logger = (Logger) LoggerFactory.getLogger(LoggingEvent_InstrumentationTest.class); + // Log at TRACE level + logger.setLevel(Level.TRACE); + + int expectedTotalEventsCapturedAtError = 1; + logger.error(CAPTURED); + + int expectedTotalEventsCapturedAtWarn = 1; + logger.warn(CAPTURED); + + int expectedTotalEventsCapturedAtInfo = 1; + logger.info(CAPTURED); + + int expectedTotalEventsCapturedAtDebug = 1; + logger.debug(CAPTURED); + + int expectedTotalEventsCapturedAtTrace = 1; + logger.trace(CAPTURED); + + int expectedTotalEventsCaptured = expectedTotalEventsCapturedAtError + + expectedTotalEventsCapturedAtWarn + expectedTotalEventsCapturedAtInfo + + expectedTotalEventsCapturedAtDebug + expectedTotalEventsCapturedAtTrace; + + Collection logEvents = introspector.getLogEvents(); + + assertEquals(expectedTotalEventsCaptured, logEvents.size()); + + List errorLevelLogEvents = getErrorLevelLogEvents(logEvents); + List warnLevelLogEvents = getWarnLevelLogEvents(logEvents); + List infoLevelLogEvents = getInfoLevelLogEvents(logEvents); + List debugLevelLogEvents = getDebugLevelLogEvents(logEvents); + List traceLevelLogEvents = getTraceLevelLogEvents(logEvents); + + assertEquals(expectedTotalEventsCapturedAtError, errorLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtWarn, warnLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtInfo, infoLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtDebug, debugLevelLogEvents.size()); + assertEquals(expectedTotalEventsCapturedAtTrace, traceLevelLogEvents.size()); + } + + private List getTraceLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.TRACE)) + .collect(Collectors.toList()); + } + + private List getDebugLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.DEBUG)) + .collect(Collectors.toList()); + } + + private List getInfoLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.INFO)) + .collect(Collectors.toList()); + } + + private List getWarnLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.WARN)) + .collect(Collectors.toList()); + } + + private List getErrorLevelLogEvents(Collection logEvents) { + return logEvents.stream() + .filter(logEvent -> logEvent.getUserAttributesCopy().containsValue(Level.ERROR)) + .collect(Collectors.toList()); + } +} diff --git a/instrumentation/logback-classic-1.2/src/test/resources/application_logging_enabled.yml b/instrumentation/logback-classic-1.2/src/test/resources/application_logging_enabled.yml new file mode 100644 index 0000000000..583ffd2bba --- /dev/null +++ b/instrumentation/logback-classic-1.2/src/test/resources/application_logging_enabled.yml @@ -0,0 +1,10 @@ +common: &default_settings + application_logging: + enabled: true + forwarding: + enabled: true + max_samples_stored: 10000 + metrics: + enabled: true + local_decorating: + enabled: false diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java index ef054e4384..c3e73e7a0e 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/AgentImpl.java @@ -12,17 +12,15 @@ import com.newrelic.agent.bridge.NoOpTransaction; import com.newrelic.agent.bridge.TracedMethod; import com.newrelic.agent.bridge.Transaction; -import com.newrelic.agent.config.AgentConfig; -import com.newrelic.agent.config.Hostname; import com.newrelic.agent.service.ServiceFactory; import com.newrelic.agent.tracers.Tracer; import com.newrelic.api.agent.Insights; import com.newrelic.api.agent.Logger; +import com.newrelic.api.agent.Logs; import com.newrelic.api.agent.MetricAggregator; import com.newrelic.api.agent.TraceMetadata; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; public class AgentImpl implements com.newrelic.agent.bridge.Agent { @@ -131,6 +129,11 @@ public Insights getInsights() { return ServiceFactory.getServiceManager().getInsights(); } + @Override + public Logs getLogSender() { + return ServiceFactory.getServiceManager().getLogSenderService(); + } + @Override public boolean startAsyncActivity(Object activityContext) { return ServiceFactory.getAsyncTxService().startAsyncActivity(activityContext); @@ -146,39 +149,9 @@ public TraceMetadata getTraceMetadata() { return TraceMetadataImpl.INSTANCE; } + @Override public Map getLinkingMetadata() { - Map linkingMetadata = new ConcurrentHashMap<>(); - - TraceMetadata traceMetadata = getTraceMetadata(); - String traceId = traceMetadata.getTraceId(); - linkingMetadata.put("trace.id", traceId); - - String spanId = traceMetadata.getSpanId(); - linkingMetadata.put("span.id", spanId); - - AgentConfig agentConfig = ServiceFactory.getConfigService().getDefaultAgentConfig(); - linkingMetadata.put("hostname", getLinkingMetaHostname(agentConfig)); - try { - String entityGuid = ServiceFactory.getRPMService().getEntityGuid(); - if (!entityGuid.isEmpty()) { - linkingMetadata.put("entity.name", agentConfig.getApplicationName()); - linkingMetadata.put("entity.type", "SERVICE"); - linkingMetadata.put("entity.guid", entityGuid); - } - } catch (NullPointerException ignored) { - // it's possible to call getLinkingMetadata in the premain before RPMService has been initialized which will NPE - Agent.LOG.log(Level.WARNING, "Cannot get entity.guid from getLinkingMetadata() until RPMService has initialized."); - } - - return linkingMetadata; - } - - private String getLinkingMetaHostname(AgentConfig agentConfig) { - String fullHostname = Hostname.getFullHostname(agentConfig); - if (fullHostname == null || fullHostname.isEmpty() || fullHostname.equals("localhost")) { - return Hostname.getHostname(agentConfig); - } - return fullHostname; + return AgentLinkingMetadata.getLinkingMetadata(getTraceMetadata(), ServiceFactory.getConfigService(), ServiceFactory.getRPMService()); } } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/AgentLinkingMetadata.java b/newrelic-agent/src/main/java/com/newrelic/agent/AgentLinkingMetadata.java new file mode 100644 index 0000000000..0b17785210 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/AgentLinkingMetadata.java @@ -0,0 +1,141 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent; + +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.config.ConfigService; +import com.newrelic.agent.config.Hostname; +import com.newrelic.api.agent.TraceMetadata; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +/** + * Utility class for providing agent linking metadata. + * This metadata can be used to link events to specific traces, spans, hosts, and entities. + */ +public class AgentLinkingMetadata { + public static final String ENTITY_TYPE_DEFAULT = "SERVICE"; + public static final String LOCALHOST = "localhost"; + // Agent linking metadata attributes + public static final String TRACE_ID = "trace.id"; + public static final String SPAN_ID = "span.id"; + public static final String HOSTNAME = "hostname"; + public static final String ENTITY_GUID = "entity.guid"; + public static final String ENTITY_NAME = "entity.name"; + public static final String ENTITY_TYPE = "entity.type"; + + /** + * Get a map of all agent linking metadata. + * + * @param traceMetadata TraceMetadataImpl instance to get spanId and traceId + * @param configService ConfigService to get hostName and entityName + * @param rpmService IRPMService to get entityGuid + * @return Map of all agent linking metadata + */ + public static Map getLinkingMetadata(TraceMetadata traceMetadata, ConfigService configService, IRPMService rpmService) { + AgentConfig agentConfig = configService.getDefaultAgentConfig(); + Map linkingMetadata = new ConcurrentHashMap<>(); + + linkingMetadata.put(TRACE_ID, getTraceId(traceMetadata)); + linkingMetadata.put(SPAN_ID, getSpanId(traceMetadata)); + linkingMetadata.put(HOSTNAME, getHostname(agentConfig)); + + try { + String entityGuid = getEntityGuid(rpmService); + if (!entityGuid.isEmpty()) { + linkingMetadata.put(ENTITY_NAME, getEntityName(agentConfig)); + linkingMetadata.put(ENTITY_TYPE, getEntityType()); + linkingMetadata.put(ENTITY_GUID, entityGuid); + } + } catch (NullPointerException ignored) { + logWarning(); + } + + return linkingMetadata; + } + + /** + * Get a map of agent linking metadata minus + * entity.type and any attributes with an empty value. + * This subset of linking metadata is added to LogEvents. + * + * @param traceMetadata TraceMetadataImpl to get spanId and traceId + * @param configService ConfigService to get hostName and entityName + * @param rpmService IRPMService to get entityGuid + * @return Filtered map of agent linking metadata + */ + public static Map getLogEventLinkingMetadata(TraceMetadata traceMetadata, ConfigService configService, IRPMService rpmService) { + AgentConfig agentConfig = configService.getDefaultAgentConfig(); + Map logEventLinkingMetadata = new ConcurrentHashMap<>(); + + String traceId = getTraceId(traceMetadata); + if (!traceId.isEmpty()) { + logEventLinkingMetadata.put(TRACE_ID, traceId); + } + + String spanId = getSpanId(traceMetadata); + if (!spanId.isEmpty()) { + logEventLinkingMetadata.put(SPAN_ID, spanId); + } + + String hostname = getHostname(agentConfig); + if (!hostname.isEmpty()) { + logEventLinkingMetadata.put(HOSTNAME, hostname); + } + try { + String entityGuid = rpmService.getEntityGuid(); + if (!entityGuid.isEmpty()) { + logEventLinkingMetadata.put(ENTITY_GUID, entityGuid); + } + String entityName = getEntityName(agentConfig); + if (!entityName.isEmpty()) { + logEventLinkingMetadata.put(ENTITY_NAME, entityName); + } + } catch (NullPointerException ignored) { + logWarning(); + } + + return logEventLinkingMetadata; + } + + public static String getTraceId(TraceMetadata traceMetadata) { + return traceMetadata.getTraceId(); + } + + public static String getSpanId(TraceMetadata traceMetadata) { + return traceMetadata.getSpanId(); + } + + private static String getHostname(AgentConfig agentConfig) { + String fullHostname = Hostname.getFullHostname(agentConfig); + if (fullHostname == null || fullHostname.isEmpty() || fullHostname.equals(LOCALHOST)) { + return Hostname.getHostname(agentConfig); + } + return fullHostname; + } + + public static String getEntityName(AgentConfig agentConfig) { + return agentConfig.getApplicationName(); + } + + public static String getEntityType() { + return ENTITY_TYPE_DEFAULT; + } + + public static String getEntityGuid(IRPMService rpmService) { + return rpmService.getEntityGuid(); + } + + private static void logWarning() { + // It's possible to call getEntityGuid in the agent premain before the + // RPMService has been initialized, which will cause a NullPointerException. + Agent.LOG.log(Level.WARNING, "Cannot get entity.guid from getLinkingMetadata() until RPMService has initialized."); + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/DummyTransaction.java b/newrelic-agent/src/main/java/com/newrelic/agent/DummyTransaction.java index 5ce4ad5bde..1c8f04b32f 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/DummyTransaction.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/DummyTransaction.java @@ -50,6 +50,7 @@ import com.newrelic.api.agent.ApplicationNamePriority; import com.newrelic.api.agent.InboundHeaders; import com.newrelic.api.agent.Insights; +import com.newrelic.api.agent.Logs; import com.newrelic.api.agent.MetricAggregator; import com.newrelic.api.agent.OutboundHeaders; import com.newrelic.api.agent.Request; @@ -83,8 +84,9 @@ public class DummyTransaction extends Transaction { private final Object lock = new Object(); private final Insights insights = new DummyInsights(); + private final Logs logs = new DummyLogs(); private final AgentConfig defaultConfig; - private final TracerList tracerList = new TracerList(null, new DummySet()); + private final TracerList tracerList = new TracerList(null, new DummySet<>()); private final TransactionTimer timer = new TransactionTimer(0); private final InboundHeaderState inboundHeaderState = new InboundHeaderState(null, null); private final SlowQueryListener slowQueryListener = new NopSlowQueryListener(); @@ -174,6 +176,11 @@ public Insights getInsightsData() { return insights; } + @Override + public Logs getLogEventData() { + return logs; + } + @Override public TransactionTracerConfig getTransactionTracerConfig() { return getAgentConfig().getTransactionTracerConfig(); @@ -655,6 +662,12 @@ public void recordCustomEvent(String eventType, Map attributes) { } } + static final class DummyLogs implements Logs { + @Override + public void recordLogEvent(Map attributes) { + } + } + static final class DummyCrossProcessState implements CrossProcessTransactionState { public static final CrossProcessTransactionState INSTANCE = new DummyCrossProcessState(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/HarvestServiceImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/HarvestServiceImpl.java index 65b373557b..2f478e5257 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/HarvestServiceImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/HarvestServiceImpl.java @@ -24,13 +24,20 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; -import static com.newrelic.agent.config.SpanEventsConfig.*; -import static com.newrelic.agent.transport.CollectorMethods.*; +import static com.newrelic.agent.config.SpanEventsConfig.SERVER_SPAN_HARVEST_CONFIG; +import static com.newrelic.agent.config.SpanEventsConfig.SERVER_SPAN_HARVEST_LIMIT; +import static com.newrelic.agent.transport.CollectorMethods.SPAN_EVENT_DATA; /** * This class is responsible for running the harvest tasks. There is one harvest task per RPM service. A harvest task @@ -85,6 +92,7 @@ public void startHarvest(IRPMService rpmService) { public void startHarvestables(IRPMService rpmService, AgentConfig config) { Map eventHarvestConfig = config.getProperty(AgentConfigFactory.EVENT_HARVEST_CONFIG); Map spanHarvestConfig = config.getProperty(SERVER_SPAN_HARVEST_CONFIG); + if (eventHarvestConfig == null) { ServiceFactory.getStatsService().doStatsWork(StatsWorks.getIncrementCounterWork( MetricNames.SUPPORTABILITY_CONNECT_MISSING_EVENT_DATA, 1), MetricNames.SUPPORTABILITY_CONNECT_MISSING_EVENT_DATA); @@ -96,36 +104,48 @@ public void startHarvestables(IRPMService rpmService, AgentConfig config) { long reportPeriodInMillis = HarvestServiceImpl.REPORTING_PERIOD_IN_MILLISECONDS; boolean isSpanEventEndpoint = tracker.harvestable.getEndpointMethodName().equals(SPAN_EVENT_DATA); - // The event_harvest_config received from server-side during the connect lifecycle contains config for error_event_data, analytic_event_data, and custom_event_data + // The event_harvest_config.harvest_limits received from server-side during the connect lifecycle + // contains config for error_event_data, analytic_event_data, custom_event_data, and log_event_data if (eventHarvestConfig != null && !isSpanEventEndpoint) { - Agent.LOG.log(Level.FINE, "event_harvest_config from collector is: {0} samples stored for {1}", maxSamplesStored, - tracker.harvestable.getEndpointMethodName()); + Agent.LOG.log(Level.FINE, "event_harvest_config from collector for {0} is: {1} max samples stored per minute", + tracker.harvestable.getEndpointMethodName(), maxSamplesStored); Map harvestLimits = (Map) eventHarvestConfig.get(HARVEST_LIMITS); + Long harvestLimit = (Long) harvestLimits.get(tracker.harvestable.getEndpointMethodName()); if (harvestLimit != null) { maxSamplesStored = harvestLimit.intValue(); - reportPeriodInMillis = (long) eventHarvestConfig.get(REPORT_PERIOD_MS); + reportPeriodInMillis = (long) eventHarvestConfig.get(REPORT_PERIOD_MS); // faster event harvest report period + float reportPeriodInSeconds = reportPeriodInMillis / 1000; + if (maxSamplesStored == 0) { + Agent.LOG.log(Level.INFO, "harvest limit has been disabled by the collector for {0}", tracker.harvestable.getEndpointMethodName()); + } + Agent.LOG.log(Level.FINE, "harvest limit from collector for {0} is: {1} max samples stored per every {2} second harvest", + tracker.harvestable.getEndpointMethodName(), harvestLimit, reportPeriodInSeconds); ServiceFactory.getStatsService().doStatsWork( - StatsWorks.getRecordMetricWork(MetricNames.SUPPORTABILITY_EVENT_HARVEST_REPORT_PERIOD_IN_SECONDS, reportPeriodInMillis / 1000), - MetricNames.SUPPORTABILITY_EVENT_HARVEST_REPORT_PERIOD_IN_SECONDS ); + StatsWorks.getRecordMetricWork(MetricNames.SUPPORTABILITY_EVENT_HARVEST_REPORT_PERIOD_IN_SECONDS, reportPeriodInSeconds), + MetricNames.SUPPORTABILITY_EVENT_HARVEST_REPORT_PERIOD_IN_SECONDS); } } else if (!isSpanEventEndpoint) { - Agent.LOG.log(Level.FINE, "event_harvest_config from collector was null. Using default value: {0} samples stored for {1}", maxSamplesStored, - tracker.harvestable.getEndpointMethodName()); + Agent.LOG.log(Level.FINE, "event_harvest_config from collector for {0} was null. Using default value: {1} max samples stored per minute", + tracker.harvestable.getEndpointMethodName(), maxSamplesStored); } // The span_event_harvest_config received from server-side during the connect lifecycle contains config for span_event_data if (spanHarvestConfig != null && isSpanEventEndpoint) { - Agent.LOG.log(Level.FINE, "span_event_harvest_config from collector is: {0} samples stored for {1}", maxSamplesStored, - tracker.harvestable.getEndpointMethodName()); + Agent.LOG.log(Level.FINE, "span_event_harvest_config from collector for {0} is: {1} max samples stored per minute", + tracker.harvestable.getEndpointMethodName(), maxSamplesStored); Long harvestLimit = (Long) spanHarvestConfig.get(SERVER_SPAN_HARVEST_LIMIT); if (harvestLimit != null) { maxSamplesStored = harvestLimit.intValue(); reportPeriodInMillis = (long) spanHarvestConfig.get(REPORT_PERIOD_MS); + float reportPeriodInSeconds = reportPeriodInMillis / 1000; + Agent.LOG.log(Level.FINE, "harvest limit from collector for {0} is: {1} max samples stored per every {2} second harvest", + tracker.harvestable.getEndpointMethodName(), harvestLimit, reportPeriodInSeconds); } } else if (isSpanEventEndpoint) { - Agent.LOG.log(Level.FINE, "span_event_harvest_config from collector was null. Using default value: {0} samples stored for {1}", maxSamplesStored, - tracker.harvestable.getEndpointMethodName()); + Agent.LOG.log(Level.FINE, + "span_event_harvest_config from collector for {0} was null. Using default value: {1} max samples stored per minute", + tracker.harvestable.getEndpointMethodName(), maxSamplesStored); } tracker.start(reportPeriodInMillis, maxSamplesStored); @@ -230,7 +250,7 @@ private ScheduledFuture scheduleHarvestTask(HarvestTask harvestTask) { /** * Get the initial delay in milliseconds. - * + *

* Tests can override. */ public long getInitialDelay() { @@ -249,7 +269,7 @@ public void setInitialDelayMillis(long millis) { /** * Get the reporting period in milliseconds. - * + *

* Tests can override. */ public long getReportingPeriod() { @@ -258,7 +278,7 @@ public long getReportingPeriod() { /** * Get the minimum harvest interval in nanoseconds. - * + *

* Tests can override. */ public long getMinHarvestInterval() { @@ -487,7 +507,8 @@ public void run() { } }; - tasks.add(scheduledFasterHarvestExecutor.scheduleAtFixedRate(SafeWrappers.safeRunnable(harvestTask), 0 , reportPeriodInMillis, TimeUnit.MILLISECONDS)); + tasks.add( + scheduledFasterHarvestExecutor.scheduleAtFixedRate(SafeWrappers.safeRunnable(harvestTask), 0, reportPeriodInMillis, TimeUnit.MILLISECONDS)); } public synchronized void stop() { diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/IRPMService.java b/newrelic-agent/src/main/java/com/newrelic/agent/IRPMService.java index dec046d9b3..aecb050f72 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/IRPMService.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/IRPMService.java @@ -11,6 +11,7 @@ import com.newrelic.agent.errors.TracedError; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.profile.ProfileData; import com.newrelic.agent.service.Service; @@ -82,6 +83,8 @@ public interface IRPMService extends Service { void sendCustomAnalyticsEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception; + void sendLogEvents(Collection events) throws Exception; + void sendErrorEvents(int reservoirSize, int eventsSeen, final Collection events) throws Exception; void sendSpanEvents(int reservoirSize, int eventsSeen, final Collection events) throws Exception; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java b/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java index 78845ce44c..5427f89fc0 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/MetricNames.java @@ -218,14 +218,27 @@ public class MetricNames { public static final String SUPPORTABILITY_INSIGHTS_SERVICE_CUSTOMER_SENT = "Supportability/Events/Customer/Sent"; public static final String SUPPORTABILITY_INSIGHTS_SERVICE_CUSTOMER_SEEN = "Supportability/Events/Customer/Seen"; + public static final String SUPPORTABILITY_LOGGING_FORWARDING_SENT = "Supportability/Logging/Forwarding/Sent"; + public static final String SUPPORTABILITY_LOGGING_FORWARDING_SEEN = "Supportability/Logging/Forwarding/Seen"; + public static final String LOGGING_FORWARDING_DROPPED = "Logging/Forwarding/Dropped"; + + public static final String SUPPORTABILITY_LOGGING_METRICS_JAVA_ENABLED = "Supportability/Logging/Metrics/Java/enabled"; + public static final String SUPPORTABILITY_LOGGING_METRICS_JAVA_DISABLED = "Supportability/Logging/Metrics/Java/disabled"; + public static final String SUPPORTABILITY_LOGGING_FORWARDING_JAVA_ENABLED = "Supportability/Logging/Forwarding/Java/enabled"; + public static final String SUPPORTABILITY_LOGGING_FORWARDING_JAVA_DISABLED = "Supportability/Logging/Forwarding/Java/disabled"; + public static final String SUPPORTABILITY_LOGGING_LOCAL_DECORATING_JAVA_ENABLED = "Supportability/Logging/LocalDecorating/Java/enabled"; + public static final String SUPPORTABILITY_LOGGING_LOCAL_DECORATING_JAVA_DISABLED = "Supportability/Logging/LocalDecorating/Java/disabled"; + public static final String SUPPORTABILITY_EVENT_HARVEST_REPORT_PERIOD_IN_SECONDS = "Supportability/EventHarvest/ReportPeriod"; public static final String SUPPORTABILITY_ERROR_SERVICE_REPORT_PERIOD_IN_SECONDS = "Supportability/EventHarvest/ErrorEventData/ReportPeriod"; public static final String SUPPORTABILITY_INSIGHTS_SERVICE_REPORT_PERIOD_IN_SECONDS = "Supportability/EventHarvest/CustomEventData/ReportPeriod"; + public static final String SUPPORTABILITY_LOG_SENDER_SERVICE_REPORT_PERIOD_IN_SECONDS = "Supportability/EventHarvest/LogEventData/ReportPeriod"; public static final String SUPPORTABILITY_SPAN_EVENT_SERVICE_REPORT_PERIOD_IN_SECONDS = "Supportability/EventHarvest/SpanEventData/ReportPeriod"; public static final String SUPPORTABILITY_ANALYTIC_EVENT_SERVICE_REPORT_PERIOD_IN_SECONDS = "Supportability/EventHarvest/AnalyticEventData/ReportPeriod"; public static final String SUPPORTABILITY_ERROR_EVENT_DATA_HARVEST_LIMIT = "Supportability/EventHarvest/ErrorEventData/HarvestLimit"; public static final String SUPPORTABILITY_CUSTOM_EVENT_DATA_HARVEST_LIMIT = "Supportability/EventHarvest/CustomEventData/HarvestLimit"; + public static final String SUPPORTABILITY_LOG_EVENT_DATA_HARVEST_LIMIT = "Supportability/EventHarvest/LogEventData/HarvestLimit"; public static final String SUPPORTABILITY_ANALYTIC_EVENT_DATA_HARVEST_LIMIT = "Supportability/EventHarvest/AnalyticEventData/HarvestLimit"; public static final String SUPPORTABILITY_SPAN_EVENT_DATA_HARVEST_LIMIT = "Supportability/EventHarvest/SpanEventData/HarvestLimit"; @@ -245,6 +258,9 @@ public class MetricNames { public static final String SUPPORTABILITY_INSIGHTS_SERVICE_EVENT_HARVEST_INTERVAL = "Supportability/EventHarvest/Customer/interval"; public static final String SUPPORTABILITY_INSIGHTS_SERVICE_EVENT_HARVEST_TRANSMIT = "Supportability/EventHarvest/Customer/transmit"; + public static final String SUPPORTABILITY_LOG_SENDER_SERVICE_EVENT_HARVEST_INTERVAL = "Supportability/EventHarvest/LogEvent/interval"; + public static final String SUPPORTABILITY_LOG_SENDER_SERVICE_EVENT_HARVEST_TRANSMIT = "Supportability/EventHarvest/LogEvent/transmit"; + public static final String SUPPORTABILITY_SPAN_SERVICE_EVENT_HARVEST_INTERVAL = "Supportability/EventHarvest/SpanEvent/interval"; public static final String SUPPORTABILITY_SPAN_SERVICE_EVENT_HARVEST_TRANSMIT = "Supportability/EventHarvest/SpanEvent/transmit"; @@ -330,6 +346,8 @@ public class MetricNames { // insights public static final String SUPPORTABILITY_API_RECORD_CUSTOM_EVENT = "RecordCustomEvent"; + public static final String SUPPORTABILITY_API_RECORD_LOG_EVENT = "RecordLogEvent"; + // attributes public static final String SUPPORTABILITY_API_ADD_CUSTOM_PARAMETER = "AddCustomParameter"; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java b/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java index ccb9d7694d..07cd45dd04 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/RPMService.java @@ -25,6 +25,7 @@ import com.newrelic.agent.model.AnalyticsEvent; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.normalization.Normalizer; import com.newrelic.agent.profile.ProfileData; @@ -141,6 +142,7 @@ private Boolean getAndLogHighSecurity(AgentConfig config) { private void addHarvestablesToServices() { ServiceFactory.getServiceManager().getInsights().addHarvestableToService(appName); + ServiceFactory.getServiceManager().getLogSenderService().addHarvestableToService(appName); ServiceFactory.getTransactionEventsService().addHarvestableToService(appName); errorService.addHarvestableToService(); ServiceFactory.getSpanEventService().addHarvestableToService(appName); @@ -529,6 +531,27 @@ public void sendCustomAnalyticsEvents(int reservoirSize, int eventsSeen, final C } } + @Override + public void sendLogEvents(final Collection events) throws Exception { + Agent.LOG.log(Level.FINE, "Sending {0} log event(s)", events.size()); + try { + sendLogEventsSyncRestart(events); + } catch (HttpError e) { + // We don't want to resend the data for certain response codes, retry for all others + if (e.isRetryableError()) { + throw e; + } + } catch (ForceRestartException e) { + logForceRestartException(e); + reconnectAsync(); + throw e; + } catch (ForceDisconnectException e) { + logForceDisconnectException(e); + shutdownAsync(); + throw e; + } + } + private void sendSpanEventsSyncRestart(int reservoirSize, int eventsSeen, final Collection events) throws Exception { try { dataSender.sendSpanEvents(reservoirSize, eventsSeen, events); @@ -571,6 +594,17 @@ private void sendCustomAnalyticsEventsSyncRestart(int reservoirSize, int eventsS } } + private void sendLogEventsSyncRestart(final Collection events) + throws Exception { + try { + dataSender.sendLogEvents(events); + } catch (ForceRestartException e) { + logForceRestartException(e); + reconnectSync(); + dataSender.sendLogEvents(events); + } + } + @Override public void sendErrorEvents(int reservoirSize, int eventsSeen, final Collection events) throws Exception { Agent.LOG.log(Level.FINE, "Sending {0} error event(s)", events.size()); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java b/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java index 3d915e2647..e284c3a300 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/Transaction.java @@ -22,6 +22,7 @@ import com.newrelic.agent.bridge.NoOpToken; import com.newrelic.agent.bridge.Token; import com.newrelic.agent.bridge.TransactionNamePriority; +import com.newrelic.api.agent.Logs; import com.newrelic.api.agent.TransportType; import com.newrelic.agent.bridge.WebResponse; import com.newrelic.agent.browser.BrowserTransactionState; @@ -191,6 +192,9 @@ public class Transaction { // Insights events added by the user during this transaction private final AtomicReference insights; + // Log events added by the user during this transaction + private final AtomicReference logEvents; + // contains all work currently running private final Map runningChildren; @@ -448,6 +452,7 @@ protected Transaction() { userAttributes = new LazyMapImpl<>(factory); errorAttributes = new LazyMapImpl<>(factory); insights = new AtomicReference<>(); + logEvents = new AtomicReference<>(); runningChildren = new LazyMapImpl<>(factory); activeTokensCache = new AtomicReference<>(); activeCount = new AtomicInteger(0); @@ -613,6 +618,16 @@ public Insights getInsightsData() { return insightsData; } + public Logs getLogEventData() { + Logs logEventData = logEvents.get(); + if (logEventData == null) { + AgentConfig defaultConfig = ServiceFactory.getConfigService().getDefaultAgentConfig(); + logEvents.compareAndSet(null, ServiceFactory.getServiceManager().getLogSenderService().getTransactionLogs(defaultConfig)); + logEventData = logEvents.get(); + } + return logEventData; + } + public TransactionTracerConfig getTransactionTracerConfig() { if (dispatcher == null) { return getAgentConfig().getTransactionTracerConfig(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/TransactionData.java b/newrelic-agent/src/main/java/com/newrelic/agent/TransactionData.java index 7d17576dd2..b5373ac1d0 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/TransactionData.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/TransactionData.java @@ -8,6 +8,7 @@ package com.newrelic.agent; import com.newrelic.agent.attributes.AttributesService; +import com.newrelic.api.agent.Logs; import com.newrelic.api.agent.TransportType; import com.newrelic.agent.config.AgentConfig; import com.newrelic.agent.config.AgentConfigImpl; @@ -48,6 +49,10 @@ public Insights getInsightsData() { return tx.getInsightsData(); } + public Logs getLogEventData() { + return tx.getLogEventData(); + } + public Dispatcher getDispatcher() { return tx.getDispatcher(); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeSender.java b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeSender.java index f7da2a95b7..50eaffc28d 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeSender.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeSender.java @@ -21,9 +21,9 @@ public AttributeSender(AttributeValidator attributeValidator) { } /** - * This is used only for logging. + * This is used only for agent logging. * - * @return The type of attribute (e.g. "agent", "custom") + * @return The type of attribute (e.g. "agent", "custom", "log") */ protected abstract String getAttributeType(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeValidator.java b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeValidator.java index 13d76d4518..31d6e65849 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeValidator.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/attributes/AttributeValidator.java @@ -12,6 +12,7 @@ import com.newrelic.agent.Agent; import com.newrelic.agent.Transaction; import com.newrelic.agent.config.ConfigConstant; +import com.newrelic.agent.service.logging.LogSenderServiceImpl; import java.math.BigDecimal; import java.math.BigInteger; @@ -161,13 +162,23 @@ private boolean validateAndLogKeyLength(String key, String methodCalled) { } private String truncateValue(String key, String value, String methodCalled) { - String truncatedVal = truncateString(value, ConfigConstant.MAX_USER_ATTRIBUTE_SIZE); + String truncatedVal; + if (methodCalled.equals(LogSenderServiceImpl.METHOD)) { + truncatedVal = truncateString(value, ConfigConstant.MAX_LOG_EVENT_ATTRIBUTE_SIZE); + logTruncatedValue(key, value, truncatedVal, methodCalled, ConfigConstant.MAX_LOG_EVENT_ATTRIBUTE_SIZE); + } else { + truncatedVal = truncateString(value, ConfigConstant.MAX_USER_ATTRIBUTE_SIZE); + logTruncatedValue(key, value, truncatedVal, methodCalled, ConfigConstant.MAX_USER_ATTRIBUTE_SIZE); + } + return truncatedVal; + } + + private void logTruncatedValue(String key, String value, String truncatedVal, String methodCalled, int maxAttributeSize) { if (!value.equals(truncatedVal)) { Agent.LOG.log(Level.FINER, "{0} was invoked with a value longer than {2} bytes for key \"{3}\". The value will be shortened to the first {4} characters.", - methodCalled, value, ConfigConstant.MAX_USER_ATTRIBUTE_SIZE, key, truncatedVal.length()); + methodCalled, value, maxAttributeSize, key, truncatedVal.length()); } - return truncatedVal; } /** @@ -176,7 +187,7 @@ private String truncateValue(String key, String value, String methodCalled) { * * @param s String to be truncated * @param maxBytes Maximum number of bytes in UTF-8 charset encoding - * @return + * @return truncated input string */ public static String truncateString(String s, int maxBytes) { int truncatedSize = 0; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java index 04c84df569..9d3b109086 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfig.java @@ -172,6 +172,13 @@ public interface AgentConfig extends com.newrelic.api.agent.Config, DataSenderCo */ InsightsConfig getInsightsConfig(); + /** + * Get the application logging configuration. + * + * @return ApplicationLoggingConfig used by LogSenderService + */ + ApplicationLoggingConfig getApplicationLoggingConfig(); + /** * Get the attributes configuration. */ diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java index 61680de690..7c36b2341a 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/AgentConfigImpl.java @@ -102,6 +102,7 @@ public class AgentConfigImpl extends BaseConfig implements AgentConfig { public static final String KEY_TRANSACTIONS = "web_transactions_apdex"; // nested configs (alphabetized) + public static final String APPLICATION_LOGGING = "application_logging"; public static final String ATTRIBUTES = "attributes"; public static final String BROWSER_MONITORING = "browser_monitoring"; public static final String CLASS_TRANSFORMER = "class_transformer"; @@ -253,6 +254,7 @@ public class AgentConfigImpl extends BaseConfig implements AgentConfig { private final ExternalTracerConfig externalTracerConfig; private final InfiniteTracingConfig infiniteTracingConfig; private final InsightsConfig insightsConfig; + private final ApplicationLoggingConfig applicationLoggingConfig; private final Config instrumentationConfig; private final JarCollectorConfig jarCollectorConfig; private final JfrConfig jfrConfig; @@ -352,6 +354,7 @@ private AgentConfigImpl(Map props) { jmxConfig = initJmxConfig(); jarCollectorConfig = initJarCollectorConfig(); insightsConfig = initInsightsConfig(); + applicationLoggingConfig = initApplicationLoggingConfig(); infiniteTracingConfig = initInfiniteTracingConfig(autoAppNamingEnabled); attributesConfig = initAttributesConfig(); reinstrumentConfig = initReinstrumentConfig(); @@ -736,6 +739,11 @@ private InsightsConfig initInsightsConfig() { return InsightsConfigImpl.createInsightsConfig(props, highSecurity); } + private ApplicationLoggingConfig initApplicationLoggingConfig() { + Map props = nestedProps(APPLICATION_LOGGING); + return ApplicationLoggingConfigImpl.createApplicationLoggingConfig(props, highSecurity); + } + private AttributesConfig initAttributesConfig() { Map props = nestedProps(ATTRIBUTES); return AttributesConfigImpl.createAttributesConfig(props); @@ -1183,6 +1191,11 @@ public InsightsConfig getInsightsConfig() { return insightsConfig; } + @Override + public ApplicationLoggingConfig getApplicationLoggingConfig() { + return applicationLoggingConfig; + } + @Override public AttributesConfig getAttributesConfig() { return attributesConfig; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingConfig.java new file mode 100644 index 0000000000..924a3f59f2 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingConfig.java @@ -0,0 +1,50 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.config; + +/** + * Configuration for application logging features. These settings do not pertain to agent logs. + */ +public interface ApplicationLoggingConfig { + + /** + * Determines whether the application_logging features are completely disabled or can be controlled individually. + * + * @return true if the application_logging features can be controlled individually, false if the entire stanza is disabled + */ + boolean isEnabled(); + + /** + * Allow metrics to be generated to provide data such as the number of lines logged at each log level. + * + * @return true is log metrics are enabled, otherwise false + */ + boolean isMetricsEnabled(); + + /** + * Allow the agent to forward application logs to New Relic. + * + * @return true is log forwarding is enabled, otherwise false + */ + boolean isForwardingEnabled(); + + /** + * Allow the agent to decorate application log files and console output with New Relic specific linking metadata. + * + * @return true is local log decorating is enabled, otherwise false + */ + boolean isLocalDecoratingEnabled(); + + /** + * Get the max number of LogEvents that can be stored during a harvest period before sampling takes place. + * + * @return max number of LogEvents stored per harvest + */ + int getMaxSamplesStored(); + +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingConfigImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingConfigImpl.java new file mode 100644 index 0000000000..f7dbab0ae7 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingConfigImpl.java @@ -0,0 +1,94 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.config; + +import java.util.Collections; +import java.util.Map; + +import static com.newrelic.agent.config.AgentConfigImpl.APPLICATION_LOGGING; + +/* Default config should look like: + * + * application_logging: + * enabled: true + * forwarding: + * enabled: false + * max_samples_stored: 10000 + * metrics: + * enabled: true + * local_decorating: + * enabled: false + */ +public class ApplicationLoggingConfigImpl extends BaseConfig implements ApplicationLoggingConfig { + public static final String SYSTEM_PROPERTY_ROOT = "newrelic.config." + APPLICATION_LOGGING + "."; + public static final String METRICS = "metrics"; + public static final String FORWARDING = "forwarding"; + public static final String LOCAL_DECORATING = "local_decorating"; + + public static final boolean DEFAULT_ENABLED = true; + public static final String ENABLED = "enabled"; + + private final ApplicationLoggingMetricsConfig applicationLoggingMetricsConfig; + private final ApplicationLoggingLocalDecoratingConfig applicationLoggingLocalDecoratingConfig; + private final ApplicationLoggingForwardingConfig applicationLoggingForwardingConfig; + + private final boolean applicationLoggingEnabled; + + public ApplicationLoggingConfigImpl(Map pProps, boolean highSecurity) { + super(pProps, SYSTEM_PROPERTY_ROOT); + applicationLoggingEnabled = getProperty(ENABLED, DEFAULT_ENABLED); + applicationLoggingMetricsConfig = createApplicationLoggingMetricsConfig(); + applicationLoggingLocalDecoratingConfig = createApplicationLoggingLocalDecoratingConfig(); + applicationLoggingForwardingConfig = createApplicationLoggingForwardingConfig(highSecurity); + } + + private ApplicationLoggingMetricsConfig createApplicationLoggingMetricsConfig() { + Map metricsProps = getProperty(METRICS, Collections.emptyMap()); + return new ApplicationLoggingMetricsConfig(metricsProps, SYSTEM_PROPERTY_ROOT); + } + + private ApplicationLoggingLocalDecoratingConfig createApplicationLoggingLocalDecoratingConfig() { + Map localDecoratingProps = getProperty(LOCAL_DECORATING, Collections.emptyMap()); + return new ApplicationLoggingLocalDecoratingConfig(localDecoratingProps, SYSTEM_PROPERTY_ROOT); + } + + private ApplicationLoggingForwardingConfig createApplicationLoggingForwardingConfig(boolean highSecurity) { + Map forwardingProps = getProperty(FORWARDING, Collections.emptyMap()); + return new ApplicationLoggingForwardingConfig(forwardingProps, SYSTEM_PROPERTY_ROOT, highSecurity); + } + + static ApplicationLoggingConfigImpl createApplicationLoggingConfig(Map settings, boolean highSecurity) { + if (settings == null) { + settings = Collections.emptyMap(); + } + return new ApplicationLoggingConfigImpl(settings, highSecurity); + } + + public boolean isEnabled() { + return applicationLoggingEnabled; + } + + @Override + public boolean isMetricsEnabled() { + return applicationLoggingEnabled && applicationLoggingMetricsConfig.getEnabled(); + } + + @Override + public boolean isLocalDecoratingEnabled() { + return applicationLoggingEnabled && applicationLoggingLocalDecoratingConfig.getEnabled(); + } + + @Override + public boolean isForwardingEnabled() { + return applicationLoggingEnabled && applicationLoggingForwardingConfig.getEnabled(); + } + + public int getMaxSamplesStored() { + return applicationLoggingForwardingConfig.getMaxSamplesStored(); + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingForwardingConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingForwardingConfig.java new file mode 100644 index 0000000000..b08d1cae0c --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingForwardingConfig.java @@ -0,0 +1,50 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.config; + +import com.newrelic.agent.Agent; + +import java.util.Map; +import java.util.logging.Level; + +public class ApplicationLoggingForwardingConfig extends BaseConfig { + public static final String ROOT = "forwarding"; + public static final String ENABLED = "enabled"; + public static final String MAX_SAMPLES_STORED = "max_samples_stored"; + + public static final boolean DEFAULT_ENABLED = false; + public static final int DEFAULT_MAX_SAMPLES_STORED = 10000; + + private final boolean enabled; + private final int maxSamplesStored; + + public ApplicationLoggingForwardingConfig(Map props, String parentRoot, boolean highSecurity) { + super(props, parentRoot + ROOT + "."); + maxSamplesStored = initMaxSamplesStored(); + boolean storedMoreThan0 = maxSamplesStored > 0; + enabled = storedMoreThan0 && !highSecurity && getProperty(ENABLED, DEFAULT_ENABLED); + } + + private int initMaxSamplesStored() { + try { + return getProperty(MAX_SAMPLES_STORED, DEFAULT_MAX_SAMPLES_STORED); + } catch (ClassCastException classCastException) { + Agent.LOG.log(Level.WARNING, "The max_samples_stored was likely too large {0}, we will use default {1}", + getProperty(MAX_SAMPLES_STORED), DEFAULT_MAX_SAMPLES_STORED); + return DEFAULT_MAX_SAMPLES_STORED; + } + } + + public boolean getEnabled() { + return enabled; + } + + public int getMaxSamplesStored() { + return maxSamplesStored; + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingLocalDecoratingConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingLocalDecoratingConfig.java new file mode 100644 index 0000000000..f0289b6534 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingLocalDecoratingConfig.java @@ -0,0 +1,28 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.config; + +import java.util.Map; + +public class ApplicationLoggingLocalDecoratingConfig extends BaseConfig { + public static final String ROOT = "local_decorating"; + public static final String ENABLED = "enabled"; + + public static final boolean DEFAULT_ENABLED = false; + + private final boolean enabled; + + public ApplicationLoggingLocalDecoratingConfig(Map props, String parentRoot) { + super(props, parentRoot + ROOT + "."); + enabled = getProperty(ENABLED, DEFAULT_ENABLED); + } + + public boolean getEnabled() { + return enabled; + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingMetricsConfig.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingMetricsConfig.java new file mode 100644 index 0000000000..25913d5760 --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ApplicationLoggingMetricsConfig.java @@ -0,0 +1,27 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.config; + +import java.util.Map; + +public class ApplicationLoggingMetricsConfig extends BaseConfig { + public static final String ROOT = "metrics"; + public static final String ENABLED = "enabled"; + public static final boolean DEFAULT_ENABLED = true; + + private final boolean enabled; + + public ApplicationLoggingMetricsConfig(Map props, String parentRoot) { + super(props, parentRoot + ROOT + "."); + enabled = getProperty(ENABLED, DEFAULT_ENABLED); + } + + public boolean getEnabled() { + return enabled; + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigConstant.java b/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigConstant.java index b7427f5d59..664b0e0dbf 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigConstant.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/config/ConfigConstant.java @@ -9,6 +9,7 @@ public class ConfigConstant { public static final int MAX_USER_ATTRIBUTES = 64; - public static final int MAX_USER_ATTRIBUTE_SIZE = 255; public static final int MAX_ERROR_MESSAGE_SIZE = 1024; + public static final int MAX_USER_ATTRIBUTE_SIZE = 255; // Size in bytes + public static final int MAX_LOG_EVENT_ATTRIBUTE_SIZE = 32767; // Size in bytes } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceFactory.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceFactory.java index 8ce469ee5b..0ba3b1ca99 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceFactory.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceFactory.java @@ -35,6 +35,7 @@ import com.newrelic.agent.service.analytics.SpanEventsService; import com.newrelic.agent.service.analytics.TransactionEventsService; import com.newrelic.agent.service.async.AsyncTransactionService; +import com.newrelic.agent.service.logging.LogSenderService; import com.newrelic.agent.service.module.JarCollectorService; import com.newrelic.agent.sql.SqlTraceService; import com.newrelic.agent.stats.StatsService; @@ -78,6 +79,10 @@ public static ConfigService getConfigService() { return SERVICE_MANAGER.getConfigService(); } + public static LogSenderService getLogSenderService() { + return SERVICE_MANAGER.getLogSenderService(); + } + public static RPMConnectionService getRPMConnectionService() { return SERVICE_MANAGER.getRPMConnectionService(); } diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManager.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManager.java index d4cdd38ab5..1f2656d2cb 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManager.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManager.java @@ -36,6 +36,7 @@ import com.newrelic.agent.service.analytics.SpanEventsService; import com.newrelic.agent.service.analytics.TransactionEventsService; import com.newrelic.agent.service.async.AsyncTransactionService; +import com.newrelic.agent.service.logging.LogSenderService; import com.newrelic.agent.service.module.JarCollectorService; import com.newrelic.agent.sql.SqlTraceService; import com.newrelic.agent.stats.StatsService; @@ -109,6 +110,8 @@ public interface ServiceManager extends Service { InsightsService getInsights(); + LogSenderService getLogSenderService(); + AsyncTransactionService getAsyncTxService(); CircuitBreakerService getCircuitBreakerService(); diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java index 75a3fe8b2e..cd58a1a041 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/ServiceManagerImpl.java @@ -61,6 +61,8 @@ import com.newrelic.agent.service.analytics.TransactionDataToDistributedTraceIntrinsics; import com.newrelic.agent.service.analytics.TransactionEventsService; import com.newrelic.agent.service.async.AsyncTransactionService; +import com.newrelic.agent.service.logging.LogSenderService; +import com.newrelic.agent.service.logging.LogSenderServiceImpl; import com.newrelic.agent.service.module.JarAnalystFactory; import com.newrelic.agent.service.module.JarCollectorConnectionListener; import com.newrelic.agent.service.module.JarCollectorHarvestListener; @@ -138,6 +140,7 @@ public class ServiceManagerImpl extends AbstractService implements ServiceManage private volatile AttributesService attsService; private volatile UtilizationService utilizationService; private volatile InsightsService insightsService; + private volatile LogSenderService logSenderService; private volatile AsyncTransactionService asyncTxService; private volatile CircuitBreakerService circuitBreakerService; private volatile DistributedTraceServiceImpl distributedTraceService; @@ -259,6 +262,7 @@ protected synchronized void doStart() throws Exception { remoteInstrumentationService = new RemoteInstrumentationServiceImpl(); attsService = new AttributesService(); insightsService = new InsightsServiceImpl(); + logSenderService = new LogSenderServiceImpl(); spanEventsService = SpanEventsServiceFactory.builder() .configService(configService) .rpmServiceManager(rpmServiceManager) @@ -301,6 +305,7 @@ protected synchronized void doStart() throws Exception { remoteInstrumentationService.start(); attsService.start(); insightsService.start(); + logSenderService.start(); circuitBreakerService.start(); distributedTraceService.start(); spanEventsService.start(); @@ -335,6 +340,7 @@ private InfiniteTracing buildInfiniteTracing(ConfigService configService) { @Override protected synchronized void doStop() throws Exception { insightsService.stop(); + logSenderService.stop(); circuitBreakerService.stop(); remoteInstrumentationService.stop(); configService.stop(); @@ -600,6 +606,11 @@ public InsightsService getInsights() { return insightsService; } + @Override + public LogSenderService getLogSenderService() { + return logSenderService; + } + @Override public CircuitBreakerService getCircuitBreakerService() { return circuitBreakerService; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderHarvestableImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderHarvestableImpl.java new file mode 100644 index 0000000000..659c0ac0aa --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderHarvestableImpl.java @@ -0,0 +1,39 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.service.logging; + +import com.newrelic.agent.Harvestable; +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.transport.CollectorMethods; + +public class LogSenderHarvestableImpl extends Harvestable { + + LogSenderHarvestableImpl(LogSenderServiceImpl logSenderService, String appName) { + super(logSenderService, appName); + } + + /** + * Agent endpoint to send log data to. + * + * @return String representing the endpoint name + */ + @Override + public String getEndpointMethodName() { + return CollectorMethods.LOG_EVENT_DATA; + } + + /** + * Number of log sender events that can be stored. + * + * @return int for max samples stored + */ + @Override + public int getMaxSamplesStored() { + return ServiceFactory.getConfigService().getDefaultAgentConfig().getApplicationLoggingConfig().getMaxSamplesStored(); + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderService.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderService.java new file mode 100644 index 0000000000..2ad06b09ed --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderService.java @@ -0,0 +1,43 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.service.logging; + +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.model.LogEvent; +import com.newrelic.agent.service.EventService; +import com.newrelic.api.agent.Insights; +import com.newrelic.api.agent.Logs; + +/** + * LogSenderService interface + * + * Extending Logs makes the recordLogEvent(...) API available to implementing classes + */ +public interface LogSenderService extends EventService, Logs { + + /** + * Returns an insights instance used to track events created during a transaction. The events will be reported to + * the Transaction's application, or to the default application if not in a transaction. + */ + Logs getTransactionLogs(AgentConfig config); + + /** + * Store event into Reservoir following usual sampling using the given appName. Preference should be given to + * storing the event in TransactionInsights instead of this. + * @param appName application name + * @param event log event + */ + void storeEvent(String appName, LogEvent event); + + /** + * Register LogSenderHarvestable + * @param appName application name + */ + void addHarvestableToService(String appName); + +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderServiceImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderServiceImpl.java new file mode 100644 index 0000000000..44feb0b67e --- /dev/null +++ b/newrelic-agent/src/main/java/com/newrelic/agent/service/logging/LogSenderServiceImpl.java @@ -0,0 +1,585 @@ +/* + * + * * Copyright 2022 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.newrelic.agent.service.logging; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.google.common.annotations.VisibleForTesting; +import com.newrelic.agent.Agent; +import com.newrelic.agent.AgentLinkingMetadata; +import com.newrelic.agent.ExtendedTransactionListener; +import com.newrelic.agent.Harvestable; +import com.newrelic.agent.MetricNames; +import com.newrelic.agent.TraceMetadataImpl; +import com.newrelic.agent.Transaction; +import com.newrelic.agent.TransactionData; +import com.newrelic.agent.attributes.AttributeSender; +import com.newrelic.agent.attributes.AttributeValidator; +import com.newrelic.agent.config.AgentConfig; +import com.newrelic.agent.config.AgentConfigListener; +import com.newrelic.agent.model.LogEvent; +import com.newrelic.agent.service.AbstractService; +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.service.analytics.DistributedSamplingPriorityQueue; +import com.newrelic.agent.stats.StatsEngine; +import com.newrelic.agent.stats.StatsService; +import com.newrelic.agent.stats.StatsWork; +import com.newrelic.agent.stats.TransactionStats; +import com.newrelic.agent.tracing.DistributedTraceServiceImpl; +import com.newrelic.agent.transport.HttpError; +import com.newrelic.api.agent.Logs; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; + +import static com.newrelic.agent.model.LogEvent.LOG_EVENT_TYPE; + +public class LogSenderServiceImpl extends AbstractService implements LogSenderService { + // Whether the service as a whole is enabled. Disabling shuts down all log events. + private volatile boolean forwardingEnabled; + // Key is the app name, value is if it is enabled - should be a limited number of names + private final ConcurrentMap isEnabledForApp = new ConcurrentHashMap<>(); + // Number of log events in the reservoir sampling buffer per-app. All apps get the same value. + private volatile int maxSamplesStored; + // Key is app name, value is collection of per-transaction log events for next harvest for that app. + private final ConcurrentHashMap> reservoirForApp = new ConcurrentHashMap<>(); + + private static final LoadingCache stringCache = Caffeine.newBuilder().maximumSize(1000) + .expireAfterAccess(70, TimeUnit.SECONDS).executor(Runnable::run).build(key -> key); + + public static final String METHOD = "add log event attribute"; + public static final String LOG_SENDER_SERVICE = "Log Sender Service"; + + /** + * Lifecycle listener for log events associated with a transaction + */ + protected final ExtendedTransactionListener transactionListener = new ExtendedTransactionListener() { + @Override + public void dispatcherTransactionStarted(Transaction transaction) { + } + + @Override + public void dispatcherTransactionFinished(TransactionData transactionData, TransactionStats transactionStats) { + // Get log events from the transaction when it is finished + TransactionLogs data = (TransactionLogs) transactionData.getLogEventData(); + storeEvents(transactionData.getApplicationName(), transactionData.getPriority(), data.events); + } + + @Override + public void dispatcherTransactionCancelled(Transaction transaction) { + // Get log events from the transaction when it is canceled + // Even if the transaction is canceled we still want to send up any events that were held in it + TransactionLogs data = (TransactionLogs) transaction.getLogEventData(); + storeEvents(transaction.getApplicationName(), transaction.getPriority(), data.events); + } + }; + + /** + * Listener to detect changes to the agent config + */ + protected final AgentConfigListener configListener = new AgentConfigListener() { + @Override + public void configChanged(String appName, AgentConfig agentConfig) { + // if the config has changed for the app, just remove it and regenerate enabled next transaction + isEnabledForApp.remove(appName); + forwardingEnabled = agentConfig.getApplicationLoggingConfig().isForwardingEnabled(); + maxSamplesStored = agentConfig.getApplicationLoggingConfig().getMaxSamplesStored(); + + boolean metricsEnabled = agentConfig.getApplicationLoggingConfig().isMetricsEnabled(); + boolean localDecoratingEnabled = agentConfig.getApplicationLoggingConfig().isLocalDecoratingEnabled(); + recordApplicationLoggingSupportabilityMetrics(forwardingEnabled, metricsEnabled, localDecoratingEnabled); + } + }; + + public void recordApplicationLoggingSupportabilityMetrics(boolean forwardingEnabled, boolean metricsEnabled, boolean localDecoratingEnabled) { + StatsService statsService = ServiceFactory.getServiceManager().getStatsService(); + + if (forwardingEnabled) { + statsService.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_LOGGING_FORWARDING_JAVA_ENABLED); + } else { + statsService.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_LOGGING_FORWARDING_JAVA_DISABLED); + } + + if (metricsEnabled) { + statsService.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_LOGGING_METRICS_JAVA_ENABLED); + } else { + statsService.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_LOGGING_METRICS_JAVA_DISABLED); + } + + if (localDecoratingEnabled) { + statsService.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_LOGGING_LOCAL_DECORATING_JAVA_ENABLED); + } else { + statsService.getMetricAggregator().incrementCounter(MetricNames.SUPPORTABILITY_LOGGING_LOCAL_DECORATING_JAVA_DISABLED); + } + } + + private final List harvestables = new ArrayList<>(); + + public LogSenderServiceImpl() { + super(LogSenderServiceImpl.class.getSimpleName()); + AgentConfig config = ServiceFactory.getConfigService().getDefaultAgentConfig(); + maxSamplesStored = config.getApplicationLoggingConfig().getMaxSamplesStored(); + forwardingEnabled = config.getApplicationLoggingConfig().isForwardingEnabled(); + isEnabledForApp.put(config.getApplicationName(), forwardingEnabled); + } + + /** + * Whether the LogSenderService is enabled or not + * @return true if enabled, else false + */ + @Override + public boolean isEnabled() { + return forwardingEnabled; + } + + /** + * Start the LogSenderService + * @throws Exception if service fails to start + */ + @Override + protected void doStart() throws Exception { + // Register transaction listener to associate log events with transaction lifecycle + ServiceFactory.getTransactionService().addTransactionListener(transactionListener); + ServiceFactory.getConfigService().addIAgentConfigListener(configListener); + } + + /** + * Stop the LogSenderService + * @throws Exception if service fails to stop + */ + @Override + protected void doStop() throws Exception { + removeHarvestables(); + ServiceFactory.getTransactionService().removeTransactionListener(transactionListener); + ServiceFactory.getConfigService().removeIAgentConfigListener(configListener); + reservoirForApp.clear(); + isEnabledForApp.clear(); + stringCache.invalidateAll(); + } + + private void removeHarvestables() { + for (Harvestable harvestable : harvestables) { + ServiceFactory.getHarvestService().removeHarvestable(harvestable); + } + } + + /** + * Records a LogEvent. If a LogEvent occurs within a Transaction it will be associated with it. + * @param attributes A map of log event data (e.g. log message, log timestamp, log level) + * Each key should be a String and each value should be a String, Number, or Boolean. + * For map values that are not String, Number, or Boolean object types the toString value will be used. + */ + @Override + public void recordLogEvent(Map attributes) { + if (logEventsDisabled() || attributes == null || attributes.isEmpty()) { + return; + } + + Transaction transaction = ServiceFactory.getTransactionService().getTransaction(false); + // Not in a Transaction or an existing Transaction is not in progress or is ignored + if (transaction == null || !transaction.isInProgress() || transaction.isIgnore()) { + String applicationName = ServiceFactory.getRPMService().getApplicationName(); + + if (transaction != null && transaction.getApplicationName() != null) { + applicationName = transaction.getApplicationName(); + } + + AgentConfig agentConfig = ServiceFactory.getConfigService().getAgentConfig(applicationName); + + if (!getIsEnabledForApp(agentConfig, applicationName)) { + reservoirForApp.remove(applicationName); + return; + } + createAndStoreEvent(applicationName, attributes); + // In a Transaction that is in progress and not ignored + } else { + // Store log events on the transaction + transaction.getLogEventData().recordLogEvent(attributes); + } + MetricNames.recordApiSupportabilityMetric(MetricNames.SUPPORTABILITY_API_RECORD_LOG_EVENT); + } + + /** + * Store a collection of LogEvents in the priority queue when a Transaction is finished or cancelled + * + * @param appName app name + * @param priority sampling priority from Transaction + * @param events collection of LogEvents to store + */ + private void storeEvents(String appName, float priority, Collection events) { + if (events.size() > 0) { + DistributedSamplingPriorityQueue eventList = getReservoir(appName); + for (LogEvent event : events) { + // Set "priority" on LogEvent based on priority value from Transaction + event.setPriority(priority); + eventList.add(event); + } + } + } + + /** + * Register LogSenderHarvestable + * @param appName application name + */ + public void addHarvestableToService(String appName) { + Harvestable harvestable = new LogSenderHarvestableImpl(this, appName); + ServiceFactory.getHarvestService().addHarvestable(harvestable); + harvestables.add(harvestable); + } + + public int getMaxSamplesStored() { + return maxSamplesStored; + } + + public void setMaxSamplesStored(int maxSamplesStored) { + this.maxSamplesStored = maxSamplesStored; + } + + public void clearReservoir() { + reservoirForApp.clear(); + } + + public void clearReservoir(String appName) { + DistributedSamplingPriorityQueue reservoir = reservoirForApp.get(appName); + if (reservoir != null) { + reservoir.clear(); + } + } + + @VisibleForTesting + void configureHarvestables(long reportPeriodInMillis, int maxSamplesStored) { + for (Harvestable h : harvestables) { + h.configure(reportPeriodInMillis, maxSamplesStored); + } + } + + @VisibleForTesting + public void harvestHarvestables() { + for (Harvestable h : harvestables) { + h.harvest(); + } + } + + public void harvestPendingEvents() { + // harvest pending events + for (String appName : reservoirForApp.keySet()) { + harvestEvents(appName); + } + } + + /** + * Store a LogEvent instance + * @param appName application name + * @param event log event + */ + @Override + public void storeEvent(String appName, LogEvent event) { + if (logEventsDisabled()) { + return; + } + + DistributedSamplingPriorityQueue eventList = getReservoir(appName); + eventList.add(event); + Agent.LOG.finest(MessageFormat.format("Added Custom Event of type {0}", event.getType())); + } + + /** + * Create and store a LogEvent instance + * @param appName application name + * @param attributes Map of attributes to create a LogEvent from + */ + private void createAndStoreEvent(String appName, Map attributes) { + if (logEventsDisabled()) { + return; + } + + DistributedSamplingPriorityQueue eventList = getReservoir(appName); + eventList.add(createValidatedEvent(attributes)); + Agent.LOG.finest(MessageFormat.format("Added event of type {0}", LOG_EVENT_TYPE)); + } + + /** + * Check if LogEvents are disabled + * + * @return true if they are disabled, false if they are enabled + */ + private boolean logEventsDisabled() { + if (!forwardingEnabled) { + if (ServiceFactory.getConfigService().getDefaultAgentConfig().isHighSecurity()) { + Agent.LOG.log(Level.FINER, "Event of type {0} not collected due to high security mode being enabled.", LOG_EVENT_TYPE); + } else { + Agent.LOG.log(Level.FINER, "Event of type {0} not collected. application_logging.forwarding not enabled.", LOG_EVENT_TYPE); + } + Agent.LOG.log(Level.FINER, "Event of type {0} not collected. application_logging.forwarding not enabled.", LOG_EVENT_TYPE); + return true; // LogEvents are disabled + } + return false; // LogEvents are enabled + } + + /** + * Get the LogEvent reservoir + * + * @param appName app name + * @return Queue of LogEvent instances + */ + @VisibleForTesting + public DistributedSamplingPriorityQueue getReservoir(String appName) { + DistributedSamplingPriorityQueue result = reservoirForApp.get(appName); + while (result == null) { + // I don't think this loop can actually execute more than once, but it's prudent to assume it can. + reservoirForApp.putIfAbsent(appName, new DistributedSamplingPriorityQueue<>(appName, LOG_SENDER_SERVICE, maxSamplesStored)); + result = reservoirForApp.get(appName); + } + return result; + } + + /** + * Harvest and send the LogEvents + * + * @param appName the application to harvest for + */ + public void harvestEvents(final String appName) { + if (!getIsEnabledForApp(ServiceFactory.getConfigService().getAgentConfig(appName), appName)) { + reservoirForApp.remove(appName); + return; + } + if (maxSamplesStored <= 0) { + clearReservoir(appName); + return; + } + + long startTimeInNanos = System.nanoTime(); + + final DistributedSamplingPriorityQueue reservoir = this.reservoirForApp.put(appName, + new DistributedSamplingPriorityQueue<>(appName, LOG_SENDER_SERVICE, maxSamplesStored)); + + if (reservoir != null && reservoir.size() > 0) { + try { + // Send LogEvents + ServiceFactory.getRPMServiceManager() + .getOrCreateRPMService(appName) + .sendLogEvents(Collections.unmodifiableList(reservoir.asList())); + + final long durationInNanos = System.nanoTime() - startTimeInNanos; + ServiceFactory.getStatsService().doStatsWork(new StatsWork() { + @Override + public void doWork(StatsEngine statsEngine) { + recordSupportabilityMetrics(statsEngine, durationInNanos, reservoir); + } + + @Override + public String getAppName() { + return appName; + } + }, LogSenderServiceImpl.class.getName()); + + if (reservoir.size() < reservoir.getNumberOfTries()) { + int dropped = reservoir.getNumberOfTries() - reservoir.size(); + Agent.LOG.log(Level.FINE, "Dropped {0} log events out of {1}.", dropped, reservoir.getNumberOfTries()); + } + } catch (HttpError e) { + if (!e.discardHarvestData()) { + Agent.LOG.log(Level.FINE, "Unable to send log events. Unsent events will be included in the next harvest.", e); + // Save unsent data by merging it with current data using reservoir algorithm + DistributedSamplingPriorityQueue currentReservoir = reservoirForApp.get(appName); + currentReservoir.retryAll(reservoir); + } else { + // discard harvest data + reservoir.clear(); + Agent.LOG.log(Level.FINE, "Unable to send log events. Unsent events will be dropped.", e); + } + } catch (Exception e) { + // discard harvest data + reservoir.clear(); + Agent.LOG.log(Level.FINE, "Unable to send log events. Unsent events will be dropped.", e); + } + } + } + + @Override + public String getEventHarvestIntervalMetric() { + return MetricNames.SUPPORTABILITY_LOG_SENDER_SERVICE_EVENT_HARVEST_INTERVAL; + } + + @Override + public String getReportPeriodInSecondsMetric() { + return MetricNames.SUPPORTABILITY_LOG_SENDER_SERVICE_REPORT_PERIOD_IN_SECONDS; + } + + @Override + public String getEventHarvestLimitMetric() { + return MetricNames.SUPPORTABILITY_LOG_EVENT_DATA_HARVEST_LIMIT; + } + + private void recordSupportabilityMetrics(StatsEngine statsEngine, long durationInNanoseconds, + DistributedSamplingPriorityQueue reservoir) { + statsEngine.getStats(MetricNames.SUPPORTABILITY_LOGGING_FORWARDING_SENT) + .incrementCallCount(reservoir.size()); + statsEngine.getStats(MetricNames.SUPPORTABILITY_LOGGING_FORWARDING_SEEN) + .incrementCallCount(reservoir.getNumberOfTries()); + + int droppedLogEvents = reservoir.getNumberOfTries() - reservoir.size(); + if (droppedLogEvents >= 0) { + statsEngine.getStats(MetricNames.LOGGING_FORWARDING_DROPPED).incrementCallCount(droppedLogEvents); + } else { + Agent.LOG.log(Level.FINE, "Invalid dropped log events value of {0}. This must be a non-negative value.", droppedLogEvents); + } + + statsEngine.getResponseTimeStats(MetricNames.SUPPORTABILITY_LOG_SENDER_SERVICE_EVENT_HARVEST_TRANSMIT) + .recordResponseTime(durationInNanoseconds, TimeUnit.NANOSECONDS); + } + + private boolean getIsEnabledForApp(AgentConfig config, String currentAppName) { + Boolean appEnabled = currentAppName == null ? null : isEnabledForApp.get(currentAppName); + if (appEnabled == null) { + appEnabled = config.getApplicationLoggingConfig().isForwardingEnabled(); + isEnabledForApp.put(currentAppName, appEnabled); + } + return appEnabled; + } + + /** + * We put Strings that occur in events in a map so that we're only ever holding a reference to one byte array for + * any given string. It's basically like interning the string without using a global map. + * + * @param value the string to "intern" + * @return the interned string + */ + private static String mapInternString(String value) { + // Note that the interning occurs on the *input* to the validation code. If the validation code truncates or + // otherwise replaces the "interned" string, the new string will not be "interned" by this cache. See the + // comment below for more information. + return stringCache.get(value); + } + + /** + * Create a validated LogEvent + * @param attributes Map of attributes to create a LogEvent from + * @return LogEvent instance + */ + private static LogEvent createValidatedEvent(Map attributes) { + Map logEventLinkingMetadata = AgentLinkingMetadata.getLogEventLinkingMetadata(TraceMetadataImpl.INSTANCE, + ServiceFactory.getConfigService(), ServiceFactory.getRPMService()); + // Initialize new logEventAttributes map with agent linking metadata + Map logEventAttributes = new HashMap<>(logEventLinkingMetadata); + + LogEvent event = new LogEvent(logEventAttributes, DistributedTraceServiceImpl.nextTruncatedFloat()); + + // Now add the attributes from the argument map to the event using an AttributeSender. + // An AttributeSender is the way to reuse all the existing attribute validations. We + // also locally "intern" Strings because we anticipate a lot of reuse of the keys and, + // possibly, the values. But there's an interaction: if the key or value is chopped + // within the attribute sender, the modified value won't be "interned" in our map. + AttributeSender sender = new LogEventAttributeSender(logEventAttributes); + + for (Map.Entry entry : attributes.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + // key or value is null, skip it with a log message and iterate to next entry in attributes.entrySet() + if (key == null || value == null) { + Agent.LOG.log(Level.WARNING, "Log event with invalid attributes key or value of null was reported for a transaction but ignored." + + " Each key should be a String and each value should be a String, Number, or Boolean."); + continue; + } + + mapInternString(key); + + if (value instanceof String) { + sender.addAttribute(key, mapInternString((String) value), METHOD); + } else if (value instanceof Number) { + sender.addAttribute(key, (Number) value, METHOD); + } else if (value instanceof Boolean) { + sender.addAttribute(key, (Boolean) value, METHOD); + } else { + // Java Agent specific - toString the value. This allows for e.g. enums as arguments. + sender.addAttribute(key, mapInternString(value.toString()), METHOD); + } + } + + return event; + } + + /** + * Validate attributes and add them to LogEvents + */ + private static class LogEventAttributeSender extends AttributeSender { + + private static final String ATTRIBUTE_TYPE = "log"; + + private final Map logEventAttributes; + + public LogEventAttributeSender(Map logEventAttributes) { + super(new AttributeValidator(ATTRIBUTE_TYPE)); + this.logEventAttributes = logEventAttributes; + setTransactional(false); + } + + @Override + protected String getAttributeType() { + return ATTRIBUTE_TYPE; + } + + @Override + protected Map getAttributeMap() { + if (ServiceFactory.getConfigService().getDefaultAgentConfig().isCustomParametersAllowed()) { + return logEventAttributes; + } + return null; + } + } + + @Override + public Logs getTransactionLogs(AgentConfig config) { + return new TransactionLogs(config); + } + + /** + * Used to record LogEvents on Transactions + */ + public static final class TransactionLogs implements Logs { + final LinkedBlockingQueue events; + + TransactionLogs(AgentConfig config) { + int maxSamplesStored = config.getApplicationLoggingConfig().getMaxSamplesStored(); + events = new LinkedBlockingQueue<>(maxSamplesStored); + } + + @Override + public void recordLogEvent(Map attributes) { + if (ServiceFactory.getConfigService().getDefaultAgentConfig().isHighSecurity()) { + Agent.LOG.log(Level.FINER, "Event of type {0} not collected due to high security mode being enabled.", LOG_EVENT_TYPE); + return; + } + + LogEvent event = createValidatedEvent(attributes); + if (events.offer(event)) { + Agent.LOG.log(Level.FINEST, "Added event of type {0} in Transaction.", LOG_EVENT_TYPE); + } else { + // Too many events are cached on the transaction, send directly to the reservoir. + String applicationName = ServiceFactory.getRPMService().getApplicationName(); + ServiceFactory.getServiceManager().getLogSenderService().storeEvent(applicationName, event); + } + } + + @VisibleForTesting + public List getEventsForTesting() { + return new ArrayList<>(events); + } + } +} diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/transport/CollectorMethods.java b/newrelic-agent/src/main/java/com/newrelic/agent/transport/CollectorMethods.java index 1483cc7299..701c45cd21 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/transport/CollectorMethods.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/transport/CollectorMethods.java @@ -19,6 +19,7 @@ public class CollectorMethods { public static final String ANALYTIC_EVENT_DATA = "analytic_event_data"; public static final String SPAN_EVENT_DATA = "span_event_data"; public static final String CUSTOM_EVENT_DATA = "custom_event_data"; + public static final String LOG_EVENT_DATA = "log_event_data"; public static final String UPDATE_LOADED_MODULES = "update_loaded_modules"; public static final String SHUTDOWN = "shutdown"; public static final String SQL_TRACE_DATA = "sql_trace_data"; diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSender.java b/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSender.java index 363b4c0c3d..165f2ffd72 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSender.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSender.java @@ -12,6 +12,7 @@ import com.newrelic.agent.model.AnalyticsEvent; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.profile.ProfileData; import com.newrelic.agent.sql.SqlTrace; @@ -44,6 +45,11 @@ public interface DataSender { */ void sendCustomAnalyticsEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception; + /** + * Send non-aggregated Log events + */ + void sendLogEvents(Collection events) throws Exception; + /** * Send non-aggregated span events */ diff --git a/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java b/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java index bcbafd4839..46ff8b7e1e 100644 --- a/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java +++ b/newrelic-agent/src/main/java/com/newrelic/agent/transport/DataSenderImpl.java @@ -24,6 +24,7 @@ import com.newrelic.agent.model.AnalyticsEvent; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.profile.ProfileData; import com.newrelic.agent.service.ServiceFactory; @@ -93,7 +94,6 @@ public class DataSenderImpl implements DataSender { // Destinations for agent data private static final String COLLECTOR = "Collector"; - private static final String OTLP = "OTLP"; // Not currently supported by Java agent // As of P17 these are the only agent endpoints that actually contain data in the response payload for a successful request private static final Set METHODS_WITH_RESPONSE_BODY = ImmutableSet.of( @@ -335,6 +335,11 @@ public void sendCustomAnalyticsEvents(int reservoirSize, int eventsSeen, Collect sendAnalyticEventsForReservoir(CollectorMethods.CUSTOM_EVENT_DATA, compressedEncoding, reservoirSize, eventsSeen, events); } + @Override + public void sendLogEvents(Collection events) throws Exception { + sendLogEventsForReservoir(CollectorMethods.LOG_EVENT_DATA, compressedEncoding, events); + } + @Override public void sendSpanEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception { sendAnalyticEventsForReservoir(CollectorMethods.SPAN_EVENT_DATA, compressedEncoding, reservoirSize, eventsSeen, events); @@ -358,6 +363,35 @@ private void sendAnalyticEventsForR invokeRunId(method, encoding, runId, params); } + // Sends LogEvent data in the MELT format for logs + // https://docs.newrelic.com/docs/logs/log-api/introduction-log-api/#log-attribute-example + private void sendLogEventsForReservoir(String method, String encoding, Collection events) throws Exception { + Object runId = agentRunId; + if (runId == NO_AGENT_RUN_ID || events.isEmpty()) { + return; + } + + JSONObject commonAttributes = new JSONObject(); + + // build attributes object + JSONObject attributes = new JSONObject(); + attributes.put("attributes", commonAttributes); + + // build common object + JSONObject common = new JSONObject(); + common.put("common", attributes); + + // build logs object + JSONObject logs = new JSONObject(); + logs.put("logs", events); + + // params is top level + InitialSizedJsonArray params = new InitialSizedJsonArray(3); + params.add(common); + params.add(logs); + invokeRunId(method, encoding, runId, params); + } + @Override public void sendMetricData(long beginTimeMillis, long endTimeMillis, List metricData) throws Exception { Object runId = agentRunId; @@ -536,7 +570,7 @@ private ReadResult connectAndSend(String host, String method, String encoding, S byte[] data = writeData(encoding, params); /* - * We don't enforce max_payload_size_in_bytes for error_data (aka error traces). Instead we halve the + * We don't enforce max_payload_size_in_bytes for error_data (aka error traces). Instead, we halve the * payload and try again. See RPMService sendErrorData */ if (data.length > maxPayloadSizeInBytes && !method.equals(CollectorMethods.ERROR_DATA)) { diff --git a/newrelic-agent/src/main/resources/newrelic.yml b/newrelic-agent/src/main/resources/newrelic.yml index 8c1be7c8c8..d3fd3e02f9 100644 --- a/newrelic-agent/src/main/resources/newrelic.yml +++ b/newrelic-agent/src/main/resources/newrelic.yml @@ -83,6 +83,43 @@ common: &default_settings # Default is the logs directory in the newrelic.jar parent directory. #log_file_path: + # Provides the ability to forward application logs to New Relic, generate log usage metrics, + # and decorate local application log files with agent metadata for use with third party log forwarders. + # The application_logging.forwarding and application_logging.local_decorating should not be used together. + application_logging: + + # Provides control over all the application logging features for forwarding, local log + # decorating, and metrics features. Set as false to disable all application logging features. + # Default is true. + enabled: true + + # The agent will automatically forward application logs to New Relic in + # a format that includes agent metadata for linking them to traces and errors. + #forwarding: + + # When true, application logs will be forwarded to New Relic. The default is false. + #enabled: false + + # Application log events are collected up to the configured amount. Afterwards, + # events are sampled to maintain an even distribution across the harvest cycle. + # Default is 10000. Setting to 0 will disable. + #max_samples_stored: 10000 + + # The agent will generate metrics to indicate the number of + # application log events occurring at each distinct log level. + metrics: + + # When true, application log metrics will be reported. The default is true. + enabled: true + + # The agent will add linking metadata to each log line in your application log files. + # This feature should only be used if you want to use a third party log forwarder, instead + # of the agent's built-in forwarding feature, to send your application log events to New Relic. + #local_decorating: + + # When true, the agent will decorate your application log files with linking metadata. The default is false. + #enabled: false + # Proxy settings for connecting to the New Relic server: # If a proxy is used, the host setting is required. Other settings # are optional. Default port is 8080. The username and password diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/AgentLinkingMetadataTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/AgentLinkingMetadataTest.java new file mode 100644 index 0000000000..adce108d9c --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/AgentLinkingMetadataTest.java @@ -0,0 +1,198 @@ +package com.newrelic.agent; + +import com.newrelic.agent.config.AgentConfigImpl; +import com.newrelic.agent.config.ConfigServiceImpl; +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.service.ServiceManagerImpl; +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AgentLinkingMetadataTest { + + @Test + public void getLinkingMetadata() { + // Given + final String expectedTraceId = "traceId1234"; + final String expectedSpanId = "spanId5678"; + final String expectedEntityGuid = "entityGuid91011"; + final String expectedEntityName = "entityName91011"; + final String expectedEntityType = AgentLinkingMetadata.ENTITY_TYPE_DEFAULT; + + TraceMetadataImpl traceMetadataMock = mock(TraceMetadataImpl.class); + ServiceManagerImpl serviceManagerMock = mock(ServiceManagerImpl.class); + RPMServiceManagerImpl rpmServiceManagerMock = mock(RPMServiceManagerImpl.class); + RPMService rpmServiceMock = mock(RPMService.class); + ConfigServiceImpl configServiceMock = mock(ConfigServiceImpl.class); + AgentConfigImpl agentConfigMock = mock(AgentConfigImpl.class); + + ServiceFactory.setServiceManager(serviceManagerMock); + + // When + when(traceMetadataMock.getTraceId()).thenReturn(expectedTraceId); + when(traceMetadataMock.getSpanId()).thenReturn(expectedSpanId); + when(serviceManagerMock.getRPMServiceManager()).thenReturn(rpmServiceManagerMock); + when(serviceManagerMock.getConfigService()).thenReturn(configServiceMock); + when(rpmServiceManagerMock.getRPMService()).thenReturn(rpmServiceMock); + when(configServiceMock.getDefaultAgentConfig()).thenReturn(agentConfigMock); + when(agentConfigMock.getApplicationName()).thenReturn(expectedEntityName); + when(rpmServiceMock.getEntityGuid()).thenReturn(expectedEntityGuid); + + // Then + Map linkingMetadata = AgentLinkingMetadata.getLinkingMetadata(traceMetadataMock, ServiceFactory.getConfigService(), + ServiceFactory.getRPMService()); + + assertFalse("linkingMetadata map shouldn't be empty", linkingMetadata.isEmpty()); + + // Can't assert on a specific hostname value as it will resolve to the actual hostname of the machine running the test + assertFalse("hostname shouldn't be empty", linkingMetadata.get(AgentLinkingMetadata.HOSTNAME).isEmpty()); + + assertEquals(expectedEntityGuid, linkingMetadata.get(AgentLinkingMetadata.ENTITY_GUID)); + assertEquals(expectedEntityName, linkingMetadata.get(AgentLinkingMetadata.ENTITY_NAME)); + assertEquals(expectedEntityType, linkingMetadata.get(AgentLinkingMetadata.ENTITY_TYPE)); + + assertEquals(expectedTraceId, linkingMetadata.get(AgentLinkingMetadata.TRACE_ID)); + assertEquals(expectedSpanId, linkingMetadata.get(AgentLinkingMetadata.SPAN_ID)); + } + + @Test + public void getLinkingMetadataWithEmptyTraceAttributes() { + // Given + final String expectedTraceId = ""; + final String expectedSpanId = ""; + final String expectedEntityGuid = "entityGuid91011"; + final String expectedEntityName = "entityName91011"; + final String expectedEntityType = AgentLinkingMetadata.ENTITY_TYPE_DEFAULT; + + TraceMetadataImpl traceMetadataMock = mock(TraceMetadataImpl.class); + ServiceManagerImpl serviceManagerMock = mock(ServiceManagerImpl.class); + RPMServiceManagerImpl rpmServiceManagerMock = mock(RPMServiceManagerImpl.class); + RPMService rpmServiceMock = mock(RPMService.class); + ConfigServiceImpl configServiceMock = mock(ConfigServiceImpl.class); + AgentConfigImpl agentConfigMock = mock(AgentConfigImpl.class); + + ServiceFactory.setServiceManager(serviceManagerMock); + + // When + when(traceMetadataMock.getTraceId()).thenReturn(expectedTraceId); + when(traceMetadataMock.getSpanId()).thenReturn(expectedSpanId); + when(serviceManagerMock.getRPMServiceManager()).thenReturn(rpmServiceManagerMock); + when(serviceManagerMock.getConfigService()).thenReturn(configServiceMock); + when(rpmServiceManagerMock.getRPMService()).thenReturn(rpmServiceMock); + when(configServiceMock.getDefaultAgentConfig()).thenReturn(agentConfigMock); + when(agentConfigMock.getApplicationName()).thenReturn(expectedEntityName); + when(rpmServiceMock.getEntityGuid()).thenReturn(expectedEntityGuid); + + // Then + Map linkingMetadata = AgentLinkingMetadata.getLinkingMetadata(traceMetadataMock, ServiceFactory.getConfigService(), + ServiceFactory.getRPMService()); + + assertFalse("linkingMetadata map shouldn't be empty", linkingMetadata.isEmpty()); + + // Can't assert on a specific hostname value as it will resolve to the actual hostname of the machine running the test + assertFalse("hostname shouldn't be empty", linkingMetadata.get(AgentLinkingMetadata.HOSTNAME).isEmpty()); + + assertEquals(expectedEntityGuid, linkingMetadata.get(AgentLinkingMetadata.ENTITY_GUID)); + assertEquals(expectedEntityName, linkingMetadata.get(AgentLinkingMetadata.ENTITY_NAME)); + assertEquals(expectedEntityType, linkingMetadata.get(AgentLinkingMetadata.ENTITY_TYPE)); + + // trace.id and span.id would be empty values if getLinkingMetadata was called outside of a transaction. + // With the getLinkingMetadata API the returned map includes keys with empty values + assertEquals(expectedTraceId, linkingMetadata.get(AgentLinkingMetadata.TRACE_ID)); + assertEquals(expectedSpanId, linkingMetadata.get(AgentLinkingMetadata.SPAN_ID)); + } + + @Test + public void getLogEventLinkingMetadata() { + // Given + final String expectedTraceId = "traceId1234"; + final String expectedSpanId = "spanId5678"; + final String expectedEntityGuid = "entityGuid91011"; + final String expectedEntityName = "entityName91011"; + + TraceMetadataImpl traceMetadataMock = mock(TraceMetadataImpl.class); + ServiceManagerImpl serviceManagerMock = mock(ServiceManagerImpl.class); + RPMServiceManagerImpl rpmServiceManagerMock = mock(RPMServiceManagerImpl.class); + RPMService rpmServiceMock = mock(RPMService.class); + ConfigServiceImpl configServiceMock = mock(ConfigServiceImpl.class); + AgentConfigImpl agentConfigMock = mock(AgentConfigImpl.class); + + ServiceFactory.setServiceManager(serviceManagerMock); + + // When + when(traceMetadataMock.getTraceId()).thenReturn(expectedTraceId); + when(traceMetadataMock.getSpanId()).thenReturn(expectedSpanId); + when(serviceManagerMock.getRPMServiceManager()).thenReturn(rpmServiceManagerMock); + when(serviceManagerMock.getConfigService()).thenReturn(configServiceMock); + when(rpmServiceManagerMock.getRPMService()).thenReturn(rpmServiceMock); + when(configServiceMock.getDefaultAgentConfig()).thenReturn(agentConfigMock); + when(agentConfigMock.getApplicationName()).thenReturn(expectedEntityName); + when(rpmServiceMock.getEntityGuid()).thenReturn(expectedEntityGuid); + + // Then + Map linkingMetadata = AgentLinkingMetadata.getLogEventLinkingMetadata(traceMetadataMock, ServiceFactory.getConfigService(), + ServiceFactory.getRPMService()); + + assertFalse("linkingMetadata map shouldn't be empty", linkingMetadata.isEmpty()); + + // Can't assert on a specific hostname value as it will resolve to the actual hostname of the machine running the test + assertFalse("hostname shouldn't be empty", linkingMetadata.get(AgentLinkingMetadata.HOSTNAME).isEmpty()); + + assertFalse("entity.type should not be included in LogEvent linking metadata", linkingMetadata.containsKey(AgentLinkingMetadata.ENTITY_TYPE)); + assertEquals(expectedEntityName, linkingMetadata.get(AgentLinkingMetadata.ENTITY_NAME)); + assertEquals(expectedEntityGuid, linkingMetadata.get(AgentLinkingMetadata.ENTITY_GUID)); + + assertEquals(expectedTraceId, linkingMetadata.get(AgentLinkingMetadata.TRACE_ID)); + assertEquals(expectedSpanId, linkingMetadata.get(AgentLinkingMetadata.SPAN_ID)); + } + + @Test + public void getLogEventLinkingMetadataWithEmptyTraceAttributes() { + // Given + final String expectedTraceId = ""; + final String expectedSpanId = ""; + final String expectedEntityGuid = "entityGuid91011"; + final String expectedEntityName = "entityName91011"; + + TraceMetadataImpl traceMetadataMock = mock(TraceMetadataImpl.class); + ServiceManagerImpl serviceManagerMock = mock(ServiceManagerImpl.class); + RPMServiceManagerImpl rpmServiceManagerMock = mock(RPMServiceManagerImpl.class); + RPMService rpmServiceMock = mock(RPMService.class); + ConfigServiceImpl configServiceMock = mock(ConfigServiceImpl.class); + AgentConfigImpl agentConfigMock = mock(AgentConfigImpl.class); + + ServiceFactory.setServiceManager(serviceManagerMock); + + // When + when(traceMetadataMock.getTraceId()).thenReturn(expectedTraceId); + when(traceMetadataMock.getSpanId()).thenReturn(expectedSpanId); + when(serviceManagerMock.getRPMServiceManager()).thenReturn(rpmServiceManagerMock); + when(serviceManagerMock.getConfigService()).thenReturn(configServiceMock); + when(rpmServiceManagerMock.getRPMService()).thenReturn(rpmServiceMock); + when(configServiceMock.getDefaultAgentConfig()).thenReturn(agentConfigMock); + when(agentConfigMock.getApplicationName()).thenReturn(expectedEntityName); + when(rpmServiceMock.getEntityGuid()).thenReturn(expectedEntityGuid); + + // Then + Map linkingMetadata = AgentLinkingMetadata.getLogEventLinkingMetadata(traceMetadataMock, ServiceFactory.getConfigService(), + ServiceFactory.getRPMService()); + + assertFalse("linkingMetadata map shouldn't be empty", linkingMetadata.isEmpty()); + + // Can't assert on a specific hostname value as it will resolve to the actual hostname of the machine running the test + assertFalse("hostname shouldn't be empty", linkingMetadata.get(AgentLinkingMetadata.HOSTNAME).isEmpty()); + + assertFalse("entity.type should not be included in LogEvent linking metadata", linkingMetadata.containsKey(AgentLinkingMetadata.ENTITY_TYPE)); + assertEquals(expectedEntityName, linkingMetadata.get(AgentLinkingMetadata.ENTITY_NAME)); + assertEquals(expectedEntityGuid, linkingMetadata.get(AgentLinkingMetadata.ENTITY_GUID)); + + // trace.id and span.id would be empty values if getLogEventLinkingMetadata was called outside of a transaction, in which case they are omitted + assertFalse("empty trace.id value should not be included in LogEvent linking metadata", linkingMetadata.containsKey(AgentLinkingMetadata.TRACE_ID)); + assertFalse("empty span.id value should not be included in LogEvent linking metadata", linkingMetadata.containsKey(AgentLinkingMetadata.SPAN_ID)); + } +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/BaseRPMService.java b/newrelic-agent/src/test/java/com/newrelic/agent/BaseRPMService.java index 2386a27ffe..5f7aeb1ba1 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/BaseRPMService.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/BaseRPMService.java @@ -13,6 +13,7 @@ import com.newrelic.agent.logging.IAgentLogger; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.profile.ProfileData; import com.newrelic.agent.service.analytics.TransactionEvent; @@ -162,6 +163,10 @@ public void sendAnalyticsEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception { } + @Override + public void sendLogEvents(Collection events) throws Exception { + } + @Override public void sendErrorEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception { } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/MockDataSender.java b/newrelic-agent/src/test/java/com/newrelic/agent/MockDataSender.java index 7ef7496927..9c461c71f6 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/MockDataSender.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/MockDataSender.java @@ -12,6 +12,7 @@ import com.newrelic.agent.model.AnalyticsEvent; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.profile.ProfileData; import com.newrelic.agent.sql.SqlTrace; @@ -20,10 +21,7 @@ import com.newrelic.agent.transport.DataSenderListener; import org.json.simple.JSONStreamAware; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.CountDownLatch; public class MockDataSender implements DataSender { @@ -34,6 +32,7 @@ public class MockDataSender implements DataSender { private static final String DATA_REPORT_PERIOD_KEY = "data_report_period"; private List traces; + private List logEvents; Map startupOptions; private Exception exception; private boolean isConnected; @@ -118,6 +117,10 @@ public List getTraces() { return traces; } + public List getLogEvents() { + return logEvents; + } + public Map getStartupOtions() { return startupOptions; } @@ -148,4 +151,12 @@ public void sendSpanEvents(int reservoirSize, int eventsSeen, final Collection events) throws Exception { } + + @Override + public void sendLogEvents(Collection events) throws Exception { + if (exception != null) { + throw exception; + } + logEvents = new ArrayList<>(events); + } } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java b/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java index dc27d80dc9..b8e3244af0 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/MockRPMService.java @@ -12,6 +12,7 @@ import com.newrelic.agent.model.AnalyticsEvent; import com.newrelic.agent.model.CustomInsightsEvent; import com.newrelic.agent.model.ErrorEvent; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.SpanEvent; import com.newrelic.agent.profile.IProfile; import com.newrelic.agent.profile.ProfileData; @@ -47,6 +48,7 @@ public class MockRPMService extends BaseRPMService { private final AtomicInteger transactionEventsSeen = new AtomicInteger(0); private final AtomicInteger spanEventsSeen = new AtomicInteger(0); private final AtomicInteger customEventsSeen = new AtomicInteger(0); + private final AtomicInteger logSenderEventsSeen = new AtomicInteger(0); private final AtomicInteger errorEventsSeen = new AtomicInteger(0); private final AtomicInteger errorTracesSeen = new AtomicInteger(0); private final AtomicInteger transactionTracesSeen = new AtomicInteger(0); @@ -222,6 +224,12 @@ public void sendCustomAnalyticsEvents(int reservoirSize, int eventsSeen, Collect this.customEventsSeen.addAndGet(eventsSeen); } + @Override + public void sendLogEvents(Collection events) throws Exception { + this.events.addAll(events); + this.logSenderEventsSeen.addAndGet(events.size()); + } + @Override public void sendErrorEvents(int reservoirSize, int eventsSeen, Collection events) throws Exception { this.events.addAll(events); @@ -261,6 +269,10 @@ public int getCustomEventsSeen() { return customEventsSeen.get(); } + public int getLogSenderEventsSeen() { + return logSenderEventsSeen.get(); + } + public int getErrorEventsSeen() { return errorEventsSeen.get(); } @@ -280,6 +292,7 @@ public void clearEvents() { spanEventsSeen.set(0); transactionEventsSeen.set(0); customEventsSeen.set(0); + logSenderEventsSeen.set(0); errorTracesSeen.set(0); transactionTracesSeen.set(0); } diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/MockServiceManager.java b/newrelic-agent/src/test/java/com/newrelic/agent/MockServiceManager.java index dbc2e742ef..6523a625f2 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/MockServiceManager.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/MockServiceManager.java @@ -38,6 +38,8 @@ import com.newrelic.agent.service.ServiceManager; import com.newrelic.agent.service.analytics.*; import com.newrelic.agent.service.async.AsyncTransactionService; +import com.newrelic.agent.service.logging.LogSenderService; +import com.newrelic.agent.service.logging.LogSenderServiceImpl; import com.newrelic.agent.service.module.JarCollectorService; import com.newrelic.agent.sql.SqlTraceService; import com.newrelic.agent.stats.StatsService; @@ -85,6 +87,7 @@ public class MockServiceManager extends AbstractService implements ServiceManage private volatile SpanEventsService spanEventsService; private volatile SourceLanguageService sourceLanguageService; private volatile InsightsService insights; + private volatile LogSenderService logSenderService; private volatile ExpirationService expirationService; public MockServiceManager() { @@ -144,6 +147,7 @@ public MockServiceManager(ConfigService configService) { circuitBreakerService = new CircuitBreakerService(); spanEventsService = Mockito.mock(SpanEventsService.class); insights = Mockito.mock(InsightsServiceImpl.class); + logSenderService = Mockito.mock(LogSenderServiceImpl.class); } @Override @@ -593,6 +597,15 @@ public void setInsights(InsightsService service) { insights = service; } + @Override + public LogSenderService getLogSenderService() { + return logSenderService; + } + + public void setLogSenderService(LogSenderService service) { + logSenderService = service; + } + @Override public CircuitBreakerService getCircuitBreakerService() { return circuitBreakerService; diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/RPMServiceTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/RPMServiceTest.java index 80d3fdded5..d630f9db4f 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/RPMServiceTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/RPMServiceTest.java @@ -25,6 +25,7 @@ import com.newrelic.agent.errors.ErrorServiceImpl; import com.newrelic.agent.errors.ThrowableError; import com.newrelic.agent.metric.MetricName; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.normalization.NormalizationRule; import com.newrelic.agent.normalization.NormalizationRuleFactory; import com.newrelic.agent.profile.IProfile; @@ -87,10 +88,7 @@ import java.util.concurrent.atomic.AtomicReference; import static java.util.Collections.singletonList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; @@ -586,6 +584,45 @@ public void transactionNamingScheme() throws Exception { assertEquals(TransactionNamingScheme.LEGACY, svc.getTransactionNamingScheme()); } + @Test(timeout = 30000) + public void testSendLogEvents() throws Exception { + Map map = createStagingMap(true, false); + map.put("app_name", "Test"); + AgentConfig config = AgentConfigImpl.createAgentConfig(map); + createServiceManager(config, map); + doSendLogEvent(); + } + + @Test(timeout = 30000) + public void testSendLogEventsWithPut() throws Exception { + Map map = createStagingMap(true, false, true); + map.put("app_name", "Test"); + AgentConfig config = AgentConfigImpl.createAgentConfig(map); + createServiceManager(config, map); + doSendLogEvent(); + } + + private void doSendLogEvent() throws Exception { + MockDataSenderFactory dataSenderFactory = new MockDataSenderFactory(); + DataSenderFactory.setDataSenderFactory(dataSenderFactory); + List appNames = singletonList("Send Log Events Test App"); + RPMService svc = new RPMService(appNames, null, null, Collections.emptyList()); + + svc.launch(); + + LogEvent logEvent1 = new LogEvent(null, 1); + LogEvent logEvent2 = new LogEvent(null, 2); + List logEvents = new ArrayList<>(); + logEvents.add(logEvent1); + logEvents.add(logEvent2); + + svc.sendLogEvents(logEvents); + + List seen = dataSenderFactory.getLastDataSender().getLogEvents(); + + assertEquals("No log events sent currently", logEvents.size(), seen.size()); + } + @Test(timeout = 30000) public void sendProfileData() throws Exception { Map map = createStagingMap(true, false); diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/attributes/AttributeValidatorTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/attributes/AttributeValidatorTest.java index e0303f801a..32e7e9ad4e 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/attributes/AttributeValidatorTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/attributes/AttributeValidatorTest.java @@ -12,6 +12,7 @@ import com.newrelic.agent.MockServiceManager; import com.newrelic.agent.Transaction; import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.service.logging.LogSenderServiceImpl; import com.newrelic.agent.tracers.ClassMethodSignature; import com.newrelic.agent.tracers.servlet.BasicRequestRootTracer; import com.newrelic.agent.tracers.servlet.MockHttpRequest; @@ -186,6 +187,25 @@ public void testVerifyTruncatedValue() { assertEquals(expected, result); } + @Test + public void testVerifyTruncatedValueForLogEventData() { + Map input = new HashMap<>(); + String longValue = Strings.padEnd("", 33000, 'e'); + String longExpectedValue = Strings.padEnd("", 32767, 'e'); + input.put("key", longValue); + input.put("apple", "pie"); + input.put("sugar", "cream"); + + Map expected = ImmutableMap.of("apple", "pie", "sugar", "cream", "key", longExpectedValue); + + AttributeValidator attributeValidator = new AttributeValidator(ATTRIBUTE_TYPE); + + attributeValidator.setTransactional(false); + Map result = attributeValidator.verifyParametersAndReturnValues(input, LogSenderServiceImpl.METHOD); + + assertEquals(expected, result); + } + @Test public void testVerifySendOutsideTxn() { String methodCalled = "noticeError"; diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/config/AgentConfigImplTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/config/AgentConfigImplTest.java index d8db9a6f24..885e74b6e2 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/config/AgentConfigImplTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/config/AgentConfigImplTest.java @@ -11,17 +11,26 @@ import com.newrelic.agent.config.internal.MapEnvironmentFacade; import com.newrelic.agent.config.internal.MapSystemProps; import com.newrelic.bootstrap.BootstrapAgent; - import org.junit.After; import org.junit.Test; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import static com.newrelic.agent.config.SpanEventsConfig.SERVER_SPAN_HARVEST_CONFIG; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /* (non-javadoc) - * Note: the "beacon" was a predecessor technology for correlated transaction traces with the browser. + * Note: the "beacon" was a predecessor technology for correlated transaction traces with the browser. * Some appearances of the term could be changed to "browser" now. */ @@ -92,7 +101,7 @@ public void collectorSetHost() { } @Test - public void defaultMetricIngestUri() { + public void defaultMetricIngestUri() { Map localMap = new HashMap<>(); // regular pre-protocol 15 key @@ -1120,7 +1129,7 @@ public void shouldLogPropertiesFromConfig() { List deprecationMessages = agentConfig.logDeprecatedProperties(settings); assertTrue(deprecationMessages.contains( - "Configuration my-prop is deprecated and will be removed in the next major version. " + "Configuration my-prop is deprecated and will be removed in the next major version. " + "It was set in the configuration file. " + "This property is obsolete." )); @@ -1143,7 +1152,6 @@ public void shouldLogPropertiesWithNewName() { )); } - @Test public void shouldLogPropertyAfterCheckingTheMap() { TestConfig target = new TestConfig(); @@ -1220,7 +1228,72 @@ public void shouldSetSpanMaxSamplesWithServerProp() { //when AgentConfig config = AgentConfigImpl.createAgentConfig(localMap); //then - assertEquals("Span max samples should be the harvest limit of: "+harvestLimit.intValue(), harvestLimit.intValue(), config.getSpanEventsConfig().getMaxSamplesStored()); + assertEquals("Span max samples should be the harvest limit of: " + harvestLimit.intValue(), harvestLimit.intValue(), + config.getSpanEventsConfig().getMaxSamplesStored()); + } + + @Test + public void getApplicationLoggingConfig() { + long NOT_DEFAULT_MAX_SAMPLES_STORED = 5000; + Map subForwardingMap = new HashMap<>(); + subForwardingMap.put(ApplicationLoggingForwardingConfig.ENABLED, !ApplicationLoggingForwardingConfig.DEFAULT_ENABLED); + subForwardingMap.put(ApplicationLoggingForwardingConfig.MAX_SAMPLES_STORED, NOT_DEFAULT_MAX_SAMPLES_STORED); + + Map subMetricMap = new HashMap<>(); + subMetricMap.put(ApplicationLoggingMetricsConfig.ENABLED, !ApplicationLoggingMetricsConfig.DEFAULT_ENABLED); + + Map subDecoratingMap = new HashMap<>(); + subDecoratingMap.put(ApplicationLoggingLocalDecoratingConfig.ENABLED, !ApplicationLoggingLocalDecoratingConfig.DEFAULT_ENABLED); + + Map loggingMap = new HashMap<>(); + loggingMap.put(ApplicationLoggingConfigImpl.FORWARDING, subForwardingMap); + loggingMap.put(ApplicationLoggingConfigImpl.METRICS, subMetricMap); + loggingMap.put(ApplicationLoggingConfigImpl.LOCAL_DECORATING, subDecoratingMap); + + Map localMap = new HashMap<>(); + localMap.put(AgentConfigImpl.APPLICATION_LOGGING, loggingMap); + AgentConfig config = AgentConfigImpl.createAgentConfig(localMap); + + assertEquals(ApplicationLoggingConfigImpl.DEFAULT_ENABLED, config.getApplicationLoggingConfig().isEnabled()); + assertTrue(config.getApplicationLoggingConfig().isForwardingEnabled()); + assertEquals(NOT_DEFAULT_MAX_SAMPLES_STORED, config.getApplicationLoggingConfig().getMaxSamplesStored()); + assertFalse(config.getApplicationLoggingConfig().isMetricsEnabled()); + assertTrue(config.getApplicationLoggingConfig().isLocalDecoratingEnabled()); + + } + + @Test + public void getApplicationLoggingConfigDefaults() { + Map localMap = new HashMap<>(); + AgentConfig config = AgentConfigImpl.createAgentConfig(localMap); + + assertTrue(config.getApplicationLoggingConfig().isEnabled()); + assertFalse(config.getApplicationLoggingConfig().isForwardingEnabled()); + assertEquals(ApplicationLoggingForwardingConfig.DEFAULT_MAX_SAMPLES_STORED, config.getApplicationLoggingConfig().getMaxSamplesStored()); + assertTrue(config.getApplicationLoggingConfig().isMetricsEnabled()); + assertFalse(config.getApplicationLoggingConfig().isLocalDecoratingEnabled()); + + } + + @Test + public void getApplicationLoggingConfigSystemProperty() { + + String key = ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT + ApplicationLoggingConfigImpl.ENABLED; + String val = String.valueOf(!ApplicationLoggingConfigImpl.DEFAULT_ENABLED); + + Map properties = new HashMap<>(); + properties.put(key, val); + + SystemPropertyProvider provider = Mocks.createSystemPropertyProvider(properties); + + Map localMap = new HashMap<>(); + Map loggingMap = new HashMap<>(); + loggingMap.put(ApplicationLoggingConfigImpl.ENABLED, provider.getSystemProperty(key)); + localMap.put(ApplicationLoggingConfigImpl.FORWARDING, loggingMap); + AgentConfig config = AgentConfigImpl.createAgentConfig(localMap); + + assertFalse(ApplicationLoggingConfigImpl.DEFAULT_ENABLED && + config.getTransactionTracerConfig().isEnabled()); } private static EnvironmentFacade createEnvironmentFacade( diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingConfigImplTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingConfigImplTest.java new file mode 100644 index 0000000000..4b608e517f --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingConfigImplTest.java @@ -0,0 +1,76 @@ +package com.newrelic.agent.config; + +import com.newrelic.agent.SaveSystemPropertyProviderRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ApplicationLoggingConfigImplTest { + + private Map localProps; + + @Rule + public SaveSystemPropertyProviderRule saveSystemPropertyProviderRule = new SaveSystemPropertyProviderRule(); + + @Before + public void setup() { + localProps = new HashMap<>(); + } + + @Test + public void testShouldBeEnabled() { + ApplicationLoggingConfigImpl config = new ApplicationLoggingConfigImpl(localProps, false); + assertTrue(config.isEnabled()); + } + + @Test + public void testDisabledOrNotWithHighSecurity() { + ApplicationLoggingConfigImpl config = new ApplicationLoggingConfigImpl(localProps, true); + assertTrue(config.isEnabled()); + assertTrue(config.isMetricsEnabled()); + + assertFalse(config.isLocalDecoratingEnabled()); + assertFalse(config.isForwardingEnabled()); + + } + + @Test + public void canConfigureViaSystemProperty() { + Properties properties = new Properties(); + //default application_logging is true + properties.put("newrelic.config.application_logging.enabled", "false"); + + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(properties), + new SaveSystemPropertyProviderRule.TestEnvironmentFacade(Collections.emptyMap()) //only test the system property + + )); + + ApplicationLoggingConfigImpl config = new ApplicationLoggingConfigImpl(Collections.emptyMap(), false); + assertFalse(config.isEnabled()); + + } + + @Test + public void canConfigureViaEnvironmentVariables() { + + //default forwarding is false + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(), //use default configs except for forwarding environment variable + new SaveSystemPropertyProviderRule.TestEnvironmentFacade(Collections.singletonMap("NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED", "true")) + )); + + ApplicationLoggingConfigImpl config = new ApplicationLoggingConfigImpl(Collections.emptyMap(), false); + + assertTrue(config.isForwardingEnabled()); + + } +} \ No newline at end of file diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingForwardingConfigTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingForwardingConfigTest.java new file mode 100644 index 0000000000..3fcd263c17 --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingForwardingConfigTest.java @@ -0,0 +1,93 @@ +package com.newrelic.agent.config; + +import com.newrelic.agent.SaveSystemPropertyProviderRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.math.BigInteger; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class ApplicationLoggingForwardingConfigTest { + private Map localProps; + private static final int TEST_MAX_SAMPLES_STORED = 5000; + + @Rule + public SaveSystemPropertyProviderRule saveSystemPropertyProviderRule = new SaveSystemPropertyProviderRule(); + + @Before + public void setup() { + localProps = new HashMap<>(); + } + + @Test + public void defaultForwardingConfig() { + ApplicationLoggingForwardingConfig config = new ApplicationLoggingForwardingConfig(localProps, ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT, + false); + assertFalse(config.getEnabled()); + + } + + @Test + public void testMaxSamplesStoredDefaultValue() { + ApplicationLoggingForwardingConfig config = new ApplicationLoggingForwardingConfig(localProps, ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT, + false); + assertEquals(ApplicationLoggingForwardingConfig.DEFAULT_MAX_SAMPLES_STORED, config.getMaxSamplesStored()); + } + + @Test + public void testMaxSamplesStoredDefaultValueIfValueTooLargeForInteger() { + Map maxSamplesStoreTooLarge = new HashMap<>(localProps); + maxSamplesStoreTooLarge.put(ApplicationLoggingForwardingConfig.MAX_SAMPLES_STORED, new BigInteger("9999999999999999999999")); + + ApplicationLoggingForwardingConfig config = new ApplicationLoggingForwardingConfig(maxSamplesStoreTooLarge, ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT, + false); + assertEquals(ApplicationLoggingForwardingConfig.DEFAULT_MAX_SAMPLES_STORED, config.getMaxSamplesStored()); + } + + @Test + public void testMaxSamplesStoredNotDefaultValue() { + localProps.put(ApplicationLoggingForwardingConfig.MAX_SAMPLES_STORED, TEST_MAX_SAMPLES_STORED); + ApplicationLoggingForwardingConfig config = new ApplicationLoggingForwardingConfig(localProps, ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT, + false); + assertEquals(TEST_MAX_SAMPLES_STORED, config.getMaxSamplesStored()); + } + + @Test + public void usesEnvVarForNestedConfig() { + + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(), + new SaveSystemPropertyProviderRule.TestEnvironmentFacade( + Collections.singletonMap("NEW_RELIC_APPLICATION_LOGGING_FORWARDING_MAX_SAMPLES_STORED", "5000")) + )); + + ApplicationLoggingForwardingConfig config = new ApplicationLoggingForwardingConfig(Collections.emptyMap(), + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT, false); + assertEquals(TEST_MAX_SAMPLES_STORED, config.getMaxSamplesStored()); + + } + + @Test + public void usesSysPropForNestedConfig() { + Properties properties = new Properties(); + + properties.put("newrelic.config.application_logging.forwarding.max_samples_stored", "" + TEST_MAX_SAMPLES_STORED); + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(properties), + new SaveSystemPropertyProviderRule.TestEnvironmentFacade() + )); + + ApplicationLoggingForwardingConfig config = new ApplicationLoggingForwardingConfig(Collections.emptyMap(), + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT, false); + assertEquals(TEST_MAX_SAMPLES_STORED, config.getMaxSamplesStored()); + + } + +} diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingLocalDecoratingConfigTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingLocalDecoratingConfigTest.java new file mode 100644 index 0000000000..5e9c1729dc --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingLocalDecoratingConfigTest.java @@ -0,0 +1,66 @@ +package com.newrelic.agent.config; + +import com.newrelic.agent.SaveSystemPropertyProviderRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ApplicationLoggingLocalDecoratingConfigTest { + + private Map localProps; + + @Rule + public SaveSystemPropertyProviderRule saveSystemPropertyProviderRule = new SaveSystemPropertyProviderRule(); + + @Before + public void setup() { + localProps = new HashMap<>(); + } + + @Test + public void defaultLocalDecoratingConfig() { + ApplicationLoggingLocalDecoratingConfig config = new ApplicationLoggingLocalDecoratingConfig(localProps, + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT); + assertFalse(config.getEnabled()); + + } + + @Test + public void usesEnvVarForLocalDecoratingConfig() { + + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(), + new SaveSystemPropertyProviderRule.TestEnvironmentFacade( + Collections.singletonMap("NEW_RELIC_APPLICATION_LOGGING_LOCAL_DECORATING_ENABLED", "true")) + )); + + ApplicationLoggingLocalDecoratingConfig config = new ApplicationLoggingLocalDecoratingConfig(Collections.emptyMap(), + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT); + assertTrue(config.getEnabled()); + + } + + @Test + public void usesSysPropForLocalDecoratingConfig() { + Properties properties = new Properties(); + + properties.put("newrelic.config.application_logging.local_decorating.enabled", "true"); + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(properties), + new SaveSystemPropertyProviderRule.TestEnvironmentFacade() + )); + + ApplicationLoggingLocalDecoratingConfig config = new ApplicationLoggingLocalDecoratingConfig(Collections.emptyMap(), + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT); + assertTrue(config.getEnabled()); + + } +} \ No newline at end of file diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingMetricsConfigTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingMetricsConfigTest.java new file mode 100644 index 0000000000..28f771ddea --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/config/ApplicationLoggingMetricsConfigTest.java @@ -0,0 +1,65 @@ +package com.newrelic.agent.config; + +import com.newrelic.agent.SaveSystemPropertyProviderRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.*; + +public class ApplicationLoggingMetricsConfigTest { + + private Map localProps; + + @Rule + public SaveSystemPropertyProviderRule saveSystemPropertyProviderRule = new SaveSystemPropertyProviderRule(); + + @Before + public void setup() { + localProps = new HashMap<>(); + } + + @Test + public void defaultLocalDecoratingConfig() { + ApplicationLoggingMetricsConfig config = new ApplicationLoggingMetricsConfig(localProps, + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT); + assertTrue(config.getEnabled()); + + } + + @Test + public void usesEnvVarForLocalDecoratingConfig() { + + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(), + new SaveSystemPropertyProviderRule.TestEnvironmentFacade( + Collections.singletonMap("NEW_RELIC_APPLICATION_LOGGING_METRICS_ENABLED", "false")) + )); + + ApplicationLoggingMetricsConfig config = new ApplicationLoggingMetricsConfig(Collections.emptyMap(), + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT); + assertFalse(config.getEnabled()); + + } + + @Test + public void usesSysPropForLocalDecoratingConfig() { + Properties properties = new Properties(); + + properties.put("newrelic.config.application_logging.metrics.enabled", "false"); + SystemPropertyFactory.setSystemPropertyProvider(new SystemPropertyProvider( + new SaveSystemPropertyProviderRule.TestSystemProps(properties), + new SaveSystemPropertyProviderRule.TestEnvironmentFacade() + )); + + ApplicationLoggingMetricsConfig config = new ApplicationLoggingMetricsConfig(Collections.emptyMap(), + ApplicationLoggingConfigImpl.SYSTEM_PROPERTY_ROOT); + assertFalse(config.getEnabled()); + + } +} \ No newline at end of file diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/service/logging/LogSenderServiceImplTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/service/logging/LogSenderServiceImplTest.java new file mode 100644 index 0000000000..430c79417e --- /dev/null +++ b/newrelic-agent/src/test/java/com/newrelic/agent/service/logging/LogSenderServiceImplTest.java @@ -0,0 +1,237 @@ +package com.newrelic.agent.service.logging; + +import com.google.common.collect.ImmutableMap; +import com.newrelic.agent.HarvestService; +import com.newrelic.agent.MockRPMService; +import com.newrelic.agent.RPMService; +import com.newrelic.agent.RPMServiceManager; +import com.newrelic.agent.Transaction; +import com.newrelic.agent.TransactionData; +import com.newrelic.agent.TransactionService; +import com.newrelic.agent.config.AgentConfigImpl; +import com.newrelic.agent.config.ApplicationLoggingConfigImpl; +import com.newrelic.agent.config.ApplicationLoggingForwardingConfig; +import com.newrelic.agent.config.ApplicationLoggingLocalDecoratingConfig; +import com.newrelic.agent.config.ApplicationLoggingMetricsConfig; +import com.newrelic.agent.config.ConfigService; +import com.newrelic.agent.service.ServiceFactory; +import com.newrelic.agent.service.ServiceManager; +import com.newrelic.agent.stats.StatsService; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class LogSenderServiceImplTest { + + private static final String appName = LogSenderServiceImplTest.class.getSimpleName() + "App"; + private static final HarvestService harvestService = Mockito.mock(HarvestService.class); + private static final TransactionService txService = Mockito.mock(TransactionService.class); + private static final StatsService statsService = Mockito.mock(StatsService.class); + + private static LogSenderServiceImpl createService() throws Exception { + return createService(createConfig()); + } + + private static LogSenderServiceImpl createService(Map config) throws Exception { + config = new HashMap<>(config); + + ServiceManager serviceManager = mock(ServiceManager.class); + when(serviceManager.getHarvestService()).thenReturn(harvestService); + when(serviceManager.getStatsService()).thenReturn(statsService); + when(serviceManager.getTransactionService()).thenReturn(txService); + when(serviceManager.getRPMServiceManager()).thenReturn(Mockito.mock(RPMServiceManager.class)); + when(serviceManager.getRPMServiceManager().getRPMService()).thenReturn(Mockito.mock(RPMService.class)); + when(serviceManager.getConfigService()).thenReturn(Mockito.mock(ConfigService.class)); + when(serviceManager.getConfigService().getDefaultAgentConfig()).thenReturn(AgentConfigImpl.createAgentConfig(config)); + when(serviceManager.getRPMServiceManager().getRPMService().getApplicationName()).thenReturn(appName); + ServiceFactory.setServiceManager(serviceManager); + + Transaction transaction = Mockito.mock(Transaction.class); + when(txService.getTransaction(false)).thenReturn(transaction); + + LogSenderServiceImpl logSenderService = new LogSenderServiceImpl(); + when(ServiceFactory.getServiceManager().getLogSenderService()).thenReturn(logSenderService); + + logSenderService.start(); + + return logSenderService; + } + + @Test + public void testHarvestableConfigure() throws Exception { + Map config = createConfig(true, 180); + LogSenderServiceImpl logSenderService = createService(config); + logSenderService.addHarvestableToService(appName); + logSenderService.configureHarvestables(60, 1); + + assertEquals(1, logSenderService.getMaxSamplesStored()); + } + + @Test + public void testHighSecurity() throws Exception { + Map config = createConfig(true, 180); + LogSenderServiceImpl logSenderService = createService(config); + + Transaction transaction = Mockito.mock(Transaction.class); + when(ServiceFactory.getTransactionService().getTransaction(false)).thenReturn(transaction); + + LogSenderServiceImpl.TransactionLogs logs = new LogSenderServiceImpl.TransactionLogs( + AgentConfigImpl.createAgentConfig(Collections.emptyMap())); + when(transaction.getLogEventData()).thenReturn(logs); + when(transaction.getApplicationName()).thenReturn(appName); + when(transaction.isInProgress()).thenReturn(true); + + logSenderService.recordLogEvent(ImmutableMap.of("field", "value")); + logSenderService.recordLogEvent(ImmutableMap.of("field2", "value2")); + logSenderService.recordLogEvent(ImmutableMap.of("field3", "value3")); + + MockRPMService analyticsData = new MockRPMService(); + when(ServiceFactory.getServiceManager().getRPMServiceManager().getRPMService(appName)).thenReturn( + analyticsData); + + TransactionData transactionData = Mockito.mock(TransactionData.class); + when(transactionData.getApplicationName()).thenReturn(appName); + when(transactionData.getLogEventData()).thenReturn(logs); + + logSenderService.transactionListener.dispatcherTransactionFinished(transactionData, null); + logSenderService.harvestHarvestables(); + + assertEquals(0, analyticsData.getEvents().size()); + assertEquals(0, logs.events.size()); + } + + @Test + public void testNoTransaction() throws Exception { + LogSenderServiceImpl logSenderService = createService(); + + logSenderService.addHarvestableToService(appName); + + verify(txService, times(1)).addTransactionListener(logSenderService.transactionListener); + + logSenderService.recordLogEvent(ImmutableMap.of("field", "value")); + logSenderService.recordLogEvent(ImmutableMap.of("field2", "value2")); + logSenderService.recordLogEvent(ImmutableMap.of("field3", "value3")); + + MockRPMService analyticsData = new MockRPMService(); + when(ServiceFactory.getServiceManager().getRPMServiceManager().getOrCreateRPMService(appName)).thenReturn( + analyticsData); + + logSenderService.harvestHarvestables(); + + assertEquals(3, analyticsData.getEvents().size()); + + logSenderService.stop(); + + verify(txService, times(1)).removeTransactionListener(logSenderService.transactionListener); + } + + @Test + public void testWithTransaction() throws Exception { + LogSenderServiceImpl logSenderService = createService(createConfig(null, 180)); + Transaction transaction = Mockito.mock(Transaction.class); + when(ServiceFactory.getTransactionService().getTransaction(false)).thenReturn(transaction); + + LogSenderServiceImpl.TransactionLogs logs = new LogSenderServiceImpl.TransactionLogs( + AgentConfigImpl.createAgentConfig(Collections.emptyMap())); + when(transaction.getLogEventData()).thenReturn(logs); + when(transaction.getApplicationName()).thenReturn(appName); + when(transaction.isInProgress()).thenReturn(true); + + logSenderService.recordLogEvent(ImmutableMap.of("field", "value")); + logSenderService.recordLogEvent(ImmutableMap.of("field2", "value2")); + logSenderService.recordLogEvent(ImmutableMap.of("field3", "value3")); + + MockRPMService analyticsData = new MockRPMService(); + when(ServiceFactory.getServiceManager().getRPMServiceManager().getOrCreateRPMService(appName)).thenReturn( + analyticsData); + + logSenderService.harvestHarvestables(); + + assertEquals(0, analyticsData.getEvents().size()); + assertEquals(3, logs.events.size()); + } + + @Test + public void testTransactionHarvest() throws Exception { + LogSenderServiceImpl logSenderService = createService(createConfig(null, 180)); + logSenderService.addHarvestableToService(appName); + + Transaction transaction = Mockito.mock(Transaction.class); + when(ServiceFactory.getTransactionService().getTransaction(false)).thenReturn(transaction); + + LogSenderServiceImpl.TransactionLogs logs = new LogSenderServiceImpl.TransactionLogs( + AgentConfigImpl.createAgentConfig(Collections.emptyMap())); + when(transaction.getLogEventData()).thenReturn(logs); + when(transaction.getApplicationName()).thenReturn(appName); + when(transaction.isInProgress()).thenReturn(true); + + logSenderService.recordLogEvent(ImmutableMap.of("field", "value")); + logSenderService.recordLogEvent(ImmutableMap.of("field2", "value2")); + logSenderService.recordLogEvent(ImmutableMap.of("field3", "value3")); + + // these should be filtered out + logSenderService.recordLogEvent(null); + logSenderService.recordLogEvent(Collections.emptyMap()); + + MockRPMService analyticsData = new MockRPMService(); + when(ServiceFactory.getServiceManager().getRPMServiceManager().getOrCreateRPMService(appName)).thenReturn( + analyticsData); + + TransactionData transactionData = Mockito.mock(TransactionData.class); + when(transactionData.getApplicationName()).thenReturn(appName); + when(transactionData.getLogEventData()).thenReturn(logs); + + logSenderService.transactionListener.dispatcherTransactionFinished(transactionData, null); + logSenderService.harvestHarvestables(); + + logSenderService.harvestHarvestables(); + + assertEquals(3, analyticsData.getEvents().size()); + } + + + private static Map createConfig() { + return createConfig(null, null, null); + } + + private static Map createConfig(Boolean highSecurity, Integer asyncTimeout) { + return createConfig(highSecurity, asyncTimeout, null); + } + + private static Map createConfig(Boolean highSecurity, Integer asyncTimeout, Long maxSamplesStored) { + Map subForwardingMap = new HashMap<>(); + subForwardingMap.put(ApplicationLoggingForwardingConfig.ENABLED, true); + subForwardingMap.put(ApplicationLoggingForwardingConfig.MAX_SAMPLES_STORED, maxSamplesStored); + + Map subMetricMap = new HashMap<>(); + subMetricMap.put(ApplicationLoggingMetricsConfig.ENABLED, true); + + Map subDecoratingMap = new HashMap<>(); + subDecoratingMap.put(ApplicationLoggingLocalDecoratingConfig.ENABLED, true); + + Map loggingMap = new HashMap<>(); + loggingMap.put(ApplicationLoggingConfigImpl.FORWARDING, subForwardingMap); + loggingMap.put(ApplicationLoggingConfigImpl.METRICS, subMetricMap); + loggingMap.put(ApplicationLoggingConfigImpl.LOCAL_DECORATING, subDecoratingMap); + + Map config = new HashMap<>(); + config.put(AgentConfigImpl.APPLICATION_LOGGING, loggingMap); + if (highSecurity != null) { + config.put(AgentConfigImpl.HIGH_SECURITY, highSecurity); + } + if (asyncTimeout != null) { + config.put(AgentConfigImpl.ASYNC_TIMEOUT, asyncTimeout); + } + config.put(AgentConfigImpl.APP_NAME, appName); + return config; + } +} \ No newline at end of file diff --git a/newrelic-agent/src/test/java/com/newrelic/agent/transport/DataSenderImplTest.java b/newrelic-agent/src/test/java/com/newrelic/agent/transport/DataSenderImplTest.java index 50c8477b4e..49ae1b3576 100644 --- a/newrelic-agent/src/test/java/com/newrelic/agent/transport/DataSenderImplTest.java +++ b/newrelic-agent/src/test/java/com/newrelic/agent/transport/DataSenderImplTest.java @@ -19,6 +19,7 @@ import com.newrelic.agent.config.AgentConfigImpl; import com.newrelic.agent.logging.IAgentLogger; import com.newrelic.agent.metric.MetricName; +import com.newrelic.agent.model.LogEvent; import com.newrelic.agent.model.PathHashes; import com.newrelic.agent.model.SpanCategory; import com.newrelic.agent.model.SpanEvent; @@ -70,6 +71,7 @@ public class DataSenderImplTest { private static final String SUPPORTABILITY_METRIC_METRIC_DATA = "Supportability/Agent/Collector/MaxPayloadSizeLimit/metric_data"; private static final String SUPPORTABILITY_METRIC_ANALYTIC_DATA = "Supportability/Agent/Collector/MaxPayloadSizeLimit/analytic_event_data"; private static final String SUPPORTABILITY_METRIC_SPAN_DATA = "Supportability/Agent/Collector/MaxPayloadSizeLimit/span_event_data"; + private static final String MAX_PAYLOAD_EXCEPTION = MaxPayloadException.class.getSimpleName(); @Rule public ExpectedException exceptionRule = ExpectedException.none(); @@ -531,6 +533,7 @@ public void testMaxPayloadSize() { sendAnalyticEventsPayloadTooBig(dataSender); sendMetricDataPayloadTooBig(dataSender); sendSpanEventsPayloadTooBig(dataSender); + sendLogEventsPayloadTooBig(dataSender); sendMetricDataSmallPayload(dataSender); @@ -596,12 +599,23 @@ private void sendMetricDataSmallPayload(DataSenderImpl dataSender) { } } + private void sendLogEventsPayloadTooBig(DataSenderImpl dataSender) { + boolean exceptionThrown = false; + try { + dataSender.sendLogEvents(createLogEvents(10000)); + } catch (Exception e) { + assertEquals(MAX_PAYLOAD_EXCEPTION, e.getClass().getSimpleName()); + exceptionThrown = true; + } + assertTrue("MaxPayloadException was NOT thrown as expected", exceptionThrown); + } + private void sendAnalyticEventsPayloadTooBig(DataSenderImpl dataSender) { try { // ~ 943 bytes dataSender.sendAnalyticsEvents(10000, 10000, createTransactionEvents(1000)); } catch (Exception e) { - assertEquals("MaxPayloadException", e.getClass().getSimpleName()); + assertEquals(MAX_PAYLOAD_EXCEPTION, e.getClass().getSimpleName()); } } @@ -610,7 +624,7 @@ private void sendMetricDataPayloadTooBig(DataSenderImpl dataSender) { // ~ 2378 bytes dataSender.sendMetricData(System.currentTimeMillis() - 60, System.currentTimeMillis(), createMetricData(1000)); } catch (Exception e) { - assertEquals("MaxPayloadException", e.getClass().getSimpleName()); + assertEquals(MAX_PAYLOAD_EXCEPTION, e.getClass().getSimpleName()); } } @@ -619,7 +633,7 @@ private void sendSpanEventsPayloadTooBig(DataSenderImpl dataSender) { // ~ 999 bytes dataSender.sendSpanEvents(10000, 10000, createSpanEvents(1000)); } catch (Exception e) { - assertEquals("MaxPayloadException", e.getClass().getSimpleName()); + assertEquals(MAX_PAYLOAD_EXCEPTION, e.getClass().getSimpleName()); } } @@ -653,6 +667,16 @@ private List createTransactionEvents(int size) { return events; } + private List createLogEvents(int size) { + List events = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Map attrs = new HashMap<>(); + attrs.put("key", "value"); + events.add(new LogEvent(attrs, 0)); + } + return events; + } + private List createMetricData(int metrics) { List metricData = new ArrayList<>(); for (int i = 0; i < metrics; i++) { diff --git a/newrelic-agent/src/test/resources/com/newrelic/agent/config/newrelic.yml b/newrelic-agent/src/test/resources/com/newrelic/agent/config/newrelic.yml index 4ae0e5b8b5..710613719e 100644 --- a/newrelic-agent/src/test/resources/com/newrelic/agent/config/newrelic.yml +++ b/newrelic-agent/src/test/resources/com/newrelic/agent/config/newrelic.yml @@ -18,6 +18,16 @@ common: &default_settings wait_for_rpm_connect: false + application_logging: + enabled: true + forwarding: + enabled: true + max_samples_stored: 10000 + metrics: + enabled: true + local_decorating: + enabled: true + # activemerchant instrumentation: only report dollar amounts (business data) # if the following is present and true; otherwise, only response times # and throughput to the gateway are reported diff --git a/settings.gradle b/settings.gradle index e788bf93a0..7e987693a0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -64,6 +64,7 @@ if (JavaVersion.current().isJava11Compatible()) { // Weaver Instrumentation include 'instrumentation:anorm-2.3' include 'instrumentation:anorm-2.4' +include 'instrumentation:apache-log4j-1' include 'instrumentation:aws-java-sdk-sqs-1.10.44' include 'instrumentation:aws-java-sdk-s3-1.2.13' include 'instrumentation:aws-java-sdk-s3-2.0' @@ -84,6 +85,7 @@ include 'instrumentation:akka-http-core-2.13_10.1.8' include 'instrumentation:akka-http-core-2.13_10.2.0' include 'instrumentation:akka-http-core-10.0' include 'instrumentation:akka-http-core-10.2.0' +include 'instrumentation:apache-log4j-2' include 'instrumentation:async-http-client-2.0.0' include 'instrumentation:async-http-client-2.1.0' include 'instrumentation:cassandra-datastax-2.1.2' @@ -124,6 +126,7 @@ include 'instrumentation:hystrix-1.3.15' include 'instrumentation:hystrix-1.4' include 'instrumentation:java.completable-future-jdk8' include 'instrumentation:java.completable-future-jdk8u40' +include 'instrumentation:java.logging-jdk8' include 'instrumentation:java-io' include 'instrumentation:javax.xml' include 'instrumentation:jax-rs-1.0' @@ -191,6 +194,7 @@ include 'instrumentation:kafka-clients-heartbeat-2.1.0' include 'instrumentation:kafka-clients-metrics-0.10.0.0' include 'instrumentation:kafka-clients-metrics-2.0.0' include 'instrumentation:kafka-clients-spans-0.11.0.0' +include 'instrumentation:logback-classic-1.2' include 'instrumentation:mongodb-async-3.4' include 'instrumentation:mongodb-async-3.6' include 'instrumentation:mongodb-async-3.7'