Skip to content

Implements sending exception attributes for log events #1228

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 8, 2022
34 changes: 27 additions & 7 deletions src/Agent/NewRelic/Agent/Core/Agent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using NewRelic.Agent.Core.Transactions;
using NewRelic.Agent.Core.Transformers.TransactionTransformer;
using NewRelic.Agent.Core.Utilities;
using NewRelic.Agent.Core.Utils;
using NewRelic.Agent.Core.WireModels;
using NewRelic.Agent.Core.Wrapper.AgentWrapperApi.Builders;
using NewRelic.Agent.Core.Wrapper.AgentWrapperApi.CrossApplicationTracing;
Expand All @@ -36,6 +37,7 @@ namespace NewRelic.Agent.Core
public class Agent : IAgent // any changes to api, update the interface in extensions and re-import, then implement in legacy api as NotImplementedException
{
public const int QueryParameterMaxStringLength = 256;
private const int LogExceptionStackLimit = 300;
internal static Agent Instance;
private static readonly ITransaction _noOpTransaction = new NoOpTransaction();

Expand Down Expand Up @@ -406,7 +408,7 @@ public void RecordSupportabilityMetric(string metricName, int count)
_agentHealthReporter.ReportSupportabilityCountMetric(metricName, count);
}

public void RecordLogMessage(string frameworkName, object logEvent, Func<object, DateTime> getTimestamp, Func<object, object> getLevel, Func<object, string> getLogMessage, string spanId, string traceId)
public void RecordLogMessage(string frameworkName, object logEvent, Func<object, DateTime> getTimestamp, Func<object, object> getLevel, Func<object, string> getLogMessage, Func<object, Exception> getLogException, string spanId, string traceId)
{
_agentHealthReporter.ReportLogForwardingFramework(frameworkName);

Expand All @@ -427,30 +429,48 @@ public void RecordLogMessage(string frameworkName, object logEvent, Func<object,
if (_configurationService.Configuration.LogEventCollectorEnabled)
{
var logMessage = getLogMessage(logEvent);
if (string.IsNullOrWhiteSpace(logMessage))
var logException = getLogException(logEvent);

// exit quickly if the message and exception are missing
if (string.IsNullOrWhiteSpace(logMessage) && logException is null)
{
return;
}

var timestamp = getTimestamp(logEvent).ToUnixTimeMilliseconds();

LogEventWireModel logEventWireModel = null;
if (logException != null)
{
logEventWireModel = new LogEventWireModel(timestamp, logMessage, normalizedLevel,
StackTraces.ScrubAndTruncate(logException, LogExceptionStackLimit), logException.Message, logException.GetType().ToString(),
spanId, traceId);
}
else
{
logEventWireModel = new LogEventWireModel(timestamp, logMessage, normalizedLevel, spanId, traceId);
}

var transaction = _transactionService.GetCurrentInternalTransaction();
if (transaction != null && transaction.IsValid)
{
// use transaction batching for messages in transactions
if (!transaction.AddLogEvent(new LogEventWireModel(timestamp, logMessage, normalizedLevel, spanId, traceId)))
if (!transaction.AddLogEvent(logEventWireModel))
{
// AddLogEvent returns false in the case that logs have already been harvested by transaction transform.
// Fall back to collecting the log based on the information we have. Since the transaction was finalized,
// the Priority should be correct.
_logEventAggregator.Collect(new LogEventWireModel(timestamp, logMessage, normalizedLevel, spanId,
traceId, transaction.Priority));
logEventWireModel.Priority = transaction.Priority;
_logEventAggregator.Collect(logEventWireModel);

}

return;
}

// non-transaction messages with proper sanitized priority value
_logEventAggregator.Collect(new LogEventWireModel(timestamp,
logMessage, normalizedLevel, spanId, traceId, _transactionService.CreatePriority()));
logEventWireModel.Priority = _transactionService.CreatePriority();
_logEventAggregator.Collect(logEventWireModel);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ public class LogEventWireModelCollectionJsonConverter : JsonConverter<LogEventWi
private const string Level = "level";
private const string SpanId = "span.id";
private const string TraceId = "trace.id";
private const string ErrorStack = "error.stack";
private const string ErrorMessage = "error.message";
private const string ErrorClass = "error.class";

public override LogEventWireModelCollection ReadJson(JsonReader reader, Type objectType, LogEventWireModelCollection existingValue, bool hasExistingValue, JsonSerializer serializer)
{
Expand Down Expand Up @@ -57,11 +60,34 @@ private static void WriteJsonImpl(JsonWriter jsonWriter, LogEventWireModelCollec
jsonWriter.WriteStartObject();
jsonWriter.WritePropertyName(TimeStamp);
jsonWriter.WriteValue(logEvent.TimeStamp);
jsonWriter.WritePropertyName(Message);
jsonWriter.WriteValue(logEvent.Message);

if (!string.IsNullOrEmpty(logEvent.Message))
{
jsonWriter.WritePropertyName(Message);
jsonWriter.WriteValue(logEvent.Message);
}

jsonWriter.WritePropertyName(Level);
jsonWriter.WriteValue(logEvent.Level);

if (!string.IsNullOrWhiteSpace(logEvent.ErrorStack))
{
jsonWriter.WritePropertyName(ErrorStack);
jsonWriter.WriteValue(logEvent.ErrorStack);
}

if (!string.IsNullOrWhiteSpace(logEvent.ErrorMessage))
{
jsonWriter.WritePropertyName(ErrorMessage);
jsonWriter.WriteValue(logEvent.ErrorMessage);
}

if (!string.IsNullOrWhiteSpace(logEvent.ErrorClass))
{
jsonWriter.WritePropertyName(ErrorClass);
jsonWriter.WriteValue(logEvent.ErrorClass);
}

if (!string.IsNullOrWhiteSpace(logEvent.SpanId))
{
jsonWriter.WritePropertyName(SpanId);
Expand Down
41 changes: 41 additions & 0 deletions src/Agent/NewRelic/Agent/Core/WireModels/LogEventWireModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using NewRelic.Collections;
using NewRelic.SystemExtensions;

Expand Down Expand Up @@ -36,6 +37,21 @@ public class LogEventWireModel : IHasPriority
/// </summary>
public string TraceId { get; }

/// <summary>
/// If present, the truncated (300 lines) stack trace of the exception stored as a single string.
/// </summary>
public string ErrorStack { get; }

/// <summary>
/// If present, the message of the exception.
/// </summary>
public string ErrorMessage { get; }

/// <summary>
/// If present, exception class name the exception.
/// </summary>
public string ErrorClass { get; }

private float _priority;
public float Priority
{
Expand Down Expand Up @@ -70,5 +86,30 @@ public LogEventWireModel(long unixTimestampMS, string message, string level, str
TraceId = traceId;
Priority = priority;
}

public LogEventWireModel(long unixTimestampMS, string message, string level, ICollection<string> errorStack, string errorMessage, string errorClass, string spanId, string traceId)
{
TimeStamp = unixTimestampMS;
Message = message.TruncateUnicodeStringByBytes(MaxMessageLengthInBytes);
Level = level;
SpanId = spanId;
TraceId = traceId;
ErrorStack = string.Join(" \n", errorStack);
ErrorMessage = errorMessage;
ErrorClass = errorClass;
}

public LogEventWireModel(long unixTimestampMS, string message, string level, ICollection<string> errorStack, string errorMessage, string errorClass, string spanId, string traceId, float priority)
{
TimeStamp = unixTimestampMS;
Message = message.TruncateUnicodeStringByBytes(MaxMessageLengthInBytes);
Level = level;
SpanId = spanId;
TraceId = traceId;
Priority = priority;
ErrorStack = string.Join(" \n", errorStack);
ErrorMessage = errorMessage;
ErrorClass = errorClass;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public interface IAgentExperimental
/// <param name="getTimestamp">A Func<object,DateTime> that knows how to get the timestamp from the logEvent.</param>
/// <param name="getLogLevel">A Func<object,object> that knows how to get the log level from the logEvent.</param>
/// <param name="getLogMessage">A Func<object,string> that knows how to get the log message from the logEvent</param>
/// <param name="getLogException">A Func<object,Exception> that knows how to get the log exception from the logEvent</param>
/// <param name="spanId">The span ID of the segment the log message occured within.</param>
/// <param name="traceId">The trace ID of the transaction the log message occured within.</param>
void RecordLogMessage(string frameworkName, object logEvent, Func<object,DateTime> getTimestamp, Func<object,object> getLogLevel, Func<object,string> getLogMessage, string spanId, string traceId);
void RecordLogMessage(string frameworkName, object logEvent, Func<object,DateTime> getTimestamp, Func<object,object> getLogLevel, Func<object,string> getLogMessage, Func<object, Exception> getLogException, string spanId, string traceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class Log4netWrapper : IWrapper
private static Func<object, object> _getLevel;
private static Func<object, string> _getRenderedMessage;
private static Func<object, DateTime> _getTimestamp;
private static Func<object, Exception> _getLogException;
private static Func<object, IDictionary> _getProperties;

public bool IsTransactionRequired => false;
Expand Down Expand Up @@ -54,9 +55,11 @@ private void RecordLogMessage(object logEvent, Type logEventType, IAgent agent)
// Older versions of log4net only allow access to a timestamp in local time
var getTimestampFunc = _getTimestamp ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<DateTime>(logEventType, "TimeStamp");

var getLogExceptionFunc = _getLogException ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<Exception>(logEventType, "ExceptionObject");

// This will either add the log message to the transaction or directly to the aggregator
var xapi = agent.GetExperimentalApi();
xapi.RecordLogMessage(WrapperName, logEvent, getTimestampFunc, getLevelFunc, getRenderedMessageFunc, agent.TraceMetadata.SpanId, agent.TraceMetadata.TraceId);
xapi.RecordLogMessage(WrapperName, logEvent, getTimestampFunc, getLevelFunc, getRenderedMessageFunc, getLogExceptionFunc, agent.TraceMetadata.SpanId, agent.TraceMetadata.TraceId);
}

private void DecorateLogMessage(object logEvent, Type logEventType, IAgent agent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using NewRelic.Agent.Extensions.Logging;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using System.Collections.Generic;
using System.Collections.Concurrent;

namespace NewRelic.Providers.Wrapper.MicrosoftExtensionsLogging
{
Expand Down Expand Up @@ -49,9 +48,11 @@ private void RecordLogMessage(MethodCall methodCall, MEL.ILogger logger, IAgent

Func<object, string> getRenderedMessageFunc = mc => ((MethodCall)mc).MethodArguments[2].ToString();

Func<object, Exception> getLogExceptionFunc = mc => ((MethodCall)mc).MethodArguments[3] as Exception; // using "as" since we want a null if missing

var xapi = agent.GetExperimentalApi();

xapi.RecordLogMessage(WrapperName, methodCall, getTimestampFunc, getLevelFunc, getRenderedMessageFunc, agent.TraceMetadata.SpanId, agent.TraceMetadata.TraceId);
xapi.RecordLogMessage(WrapperName, methodCall, getTimestampFunc, getLevelFunc, getRenderedMessageFunc, getLogExceptionFunc, agent.TraceMetadata.SpanId, agent.TraceMetadata.TraceId);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class NLogWrapper : IWrapper
private static Func<object, string> _getRenderedMessage;
private static Func<object, DateTime> _getTimestamp;
private static Func<object, string> _messageGetter;
private static Func<object, Exception> _getLogException;

public bool IsTransactionRequired => false;

Expand Down Expand Up @@ -51,9 +52,11 @@ private void RecordLogMessage(object logEvent, Type logEventType, IAgent agent)

var getTimestampFunc = _getTimestamp ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<DateTime>(logEventType, "TimeStamp");

var getLogExceptionFunc = _getLogException ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<Exception>(logEventType, "Exception");

// This will either add the log message to the transaction or directly to the aggregator
var xapi = agent.GetExperimentalApi();
xapi.RecordLogMessage(WrapperName, logEvent, getTimestampFunc, getLevelFunc, getRenderedMessageFunc, agent.TraceMetadata.SpanId, agent.TraceMetadata.TraceId);
xapi.RecordLogMessage(WrapperName, logEvent, getTimestampFunc, getLevelFunc, getRenderedMessageFunc, getLogExceptionFunc, agent.TraceMetadata.SpanId, agent.TraceMetadata.TraceId);
}

private void DecorateLogMessage(object logEvent, Type logEventType, IAgent agent)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ private void RecordLogMessage(LogEvent logEvent)

Func<object, DateTime> getDateTimeFunc = l => logEvent.Timestamp.UtcDateTime;

Func<object, string> getMessageFunc = l => logEvent.RenderMessage();
Func<object, Exception> getLogExceptionFunc = l => logEvent.Exception;

// This will either add the log message to the transaction or directly to the aggregator
Func<object, string> getMessageFunc = l => logEvent.RenderMessage();

var xapi = _agent.GetExperimentalApi();
xapi.RecordLogMessage("serilog", logEvent, getDateTimeFunc, getLevelFunc, getMessageFunc, _agent.TraceMetadata.SpanId, _agent.TraceMetadata.TraceId);
xapi.RecordLogMessage("serilog", logEvent, getDateTimeFunc, getLevelFunc, getMessageFunc, getLogExceptionFunc, _agent.TraceMetadata.SpanId, _agent.TraceMetadata.TraceId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,30 @@ private static LogLine TryFindLogLine(ExpectedLogLine expectedLogLine, IEnumerab
if (expectedLogLine.HasTraceId.HasValue && !expectedLogLine.HasTraceId.Value && !string.IsNullOrWhiteSpace(actualLogLine.Traceid))
continue;

if (expectedLogLine.HasException.HasValue && expectedLogLine.HasException.Value)
{
if (string.IsNullOrWhiteSpace(actualLogLine.ErrorStack)
|| string.IsNullOrWhiteSpace(actualLogLine.ErrorMessage)
|| string.IsNullOrWhiteSpace(actualLogLine.ErrorClass)
)
continue;

if (!actualLogLine.ErrorStack.Contains(expectedLogLine.ErrorStack))
continue;
if (expectedLogLine.ErrorMessage != actualLogLine.ErrorMessage)
continue;
if (expectedLogLine.ErrorClass != actualLogLine.ErrorClass)
continue;
}

if (expectedLogLine.HasException.HasValue && !expectedLogLine.HasException.Value)
{
if (!string.IsNullOrWhiteSpace(actualLogLine.ErrorStack)
|| !string.IsNullOrWhiteSpace(actualLogLine.ErrorMessage)
|| !string.IsNullOrWhiteSpace(actualLogLine.ErrorClass))
continue;
}

return actualLogLine;
}

Expand Down Expand Up @@ -1034,9 +1058,14 @@ public class ExpectedLogLine
public bool? HasSpanId = null;
public bool? HasTraceId = null;

public bool? HasException = null;
public string ErrorStack = null;
public string ErrorMessage = null;
public string ErrorClass = null;

public override string ToString()
{
return $"{{ Level: {Level}, LogMessage: {LogMessage}, HasSpanId: {HasSpanId}, HasTraceId: {HasTraceId} }}";
return $"{{ Level: {Level}, LogMessage: {LogMessage}, HasSpanId: {HasSpanId}, HasTraceId: {HasTraceId} }}, HasException: {HasException}, ErrorStack: {ErrorStack}, ErrorMessage: {ErrorMessage}, ErrorClass: {ErrorClass}";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class LogLine
[JsonProperty("timestamp")]
public long Timestamp { get; set; }

[JsonProperty("message")]
[JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)]
public string Message { get; set; }

[JsonProperty("level")]
Expand All @@ -65,5 +65,14 @@ public class LogLine

[JsonProperty("trace.id", NullValueHandling = NullValueHandling.Ignore)]
public string Traceid { get; set; }

[JsonProperty("error.stack", NullValueHandling = NullValueHandling.Ignore)]
public string ErrorStack { get; set; }

[JsonProperty("error.message", NullValueHandling = NullValueHandling.Ignore)]
public string ErrorMessage { get; set; }

[JsonProperty("error.class", NullValueHandling = NullValueHandling.Ignore)]
public string ErrorClass { get; set; }
}
}
20 changes: 18 additions & 2 deletions tests/Agent/IntegrationTests/IntegrationTests/Logging/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ public static string GetLevelName(LoggingFramework loggingFramework, string leve
{
// log4net names are the same as our internal names
case LoggingFramework.Log4net:
return level;
switch (level)
{
case "NOMESSAGE":
return "ERROR";
default:
return level;
}
case LoggingFramework.MicrosoftLogging:
switch (level)
{
Expand All @@ -52,6 +58,8 @@ public static string GetLevelName(LoggingFramework loggingFramework, string leve
return "ERROR";
case "FATAL":
return "CRITICAL";
case "NOMESSAGE":
return "ERROR";
default:
return level;
}
Expand All @@ -69,11 +77,19 @@ public static string GetLevelName(LoggingFramework loggingFramework, string leve
return "ERROR";
case "FATAL":
return "FATAL";
case "NOMESSAGE":
return "ERROR";
default:
return level;
}
case LoggingFramework.NLog:
return level;
switch (level)
{
case "NOMESSAGE":
return "ERROR";
default:
return level;
}
}

return string.Empty;
Expand Down
Loading