Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public class MicrosoftLoggingWrapper : IWrapper

private const string WrapperName = "MicrosoftLogging";

// This is defined here: https://github.com/dotnet/runtime/blob/54e4456b01060f54e5bb7d715e77e5f41bac558f/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogValuesFormatter.cs#L233
private const string OriginalFormatKey = "{OriginalFormat}";

public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
{
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
Expand Down Expand Up @@ -58,14 +61,18 @@ private void RecordLogMessage(MethodCall methodCall, MEL.ILogger logger, IAgent
Func<object, string> getLevelFunc = mc => ((MethodCall)mc).MethodArguments[0].ToString();
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
Func<object, Dictionary<string, object>> getContextDataFunc = _ => GetContextData(logger, agent);

// MethodArguments[2] is the TState state parameter, which for structured logging
// typically implements IReadOnlyList<KeyValuePair<string, object?>>
var state = methodCall.MethodArguments[2];
Func<object, Dictionary<string, object>> getContextDataFunc = _ => GetContextData(logger, agent, state);

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

private static Dictionary<string, object> GetContextData(MEL.ILogger logger, IAgent agent)
private static Dictionary<string, object> GetContextData(MEL.ILogger logger, IAgent agent, object state = null)
{
if (_contextDataNotSupported) // short circuit if we previously got an exception trying to access context data
{
Expand Down Expand Up @@ -106,6 +113,35 @@ private static Dictionary<string, object> GetContextData(MEL.ILogger logger, IAg
// Possibly handle case of IEnumerable<KeyValuePair<object, object>>, etc (not now though)
}, harvestedKvps);

// Extract structured log message arguments from the TState state parameter.
// For structured logging (e.g., _logger.LogInformation("User {UserId} did {Action}", userId, action)),
// MEL's FormattedLogValues implements IReadOnlyList<KeyValuePair<string, object?>> containing
// the individual named parameters. We merge these into context data so they appear as
// separate log attributes in New Relic (e.g., context.UserId, context.Action).
try
{
if (state is IEnumerable<KeyValuePair<string, object>> stateKvps)
{
foreach (var kvp in stateKvps)
{
// Skip the "{OriginalFormat}" key — it's the message template string, not a parameter
if (kvp.Key == OriginalFormatKey)
continue;

if (kvp.Value != null)
{
harvestedKvps[kvp.Key] = kvp.Value;
}
}
}
}
catch (Exception e)
{
// Log once and continue — state extraction is best-effort and should not
// prevent scope-based context data from being returned
agent.Logger.Log(Level.Finest, e, "Unable to extract structured log message arguments from state.");
}

return harvestedKvps;
}
catch (Exception e)
Expand Down Expand Up @@ -135,4 +171,4 @@ private AfterWrappedMethodDelegate DecorateLogMessage(MEL.ILogger logger, IAgent

return Delegates.GetDelegateFor(onComplete: () => handle?.Dispose());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ public ContextDataNotSupportedTestsBase(TFixture fixture, ITestOutputHelper outp
string context = string.Join(",", _expectedAttributes.Select(x => x.Key + "=" + x.Value).ToArray());

// should generate an exception message in the log since the dummy ILogger doesn't have the right properties
_fixture.AddCommand($"LoggingTester CreateSingleLogMessage {InfoMessage} INFO {context}");
_fixture.AddCommand($"LoggingTester CreateSingleLogMessage {InfoMessage} {context}");
// do it again - this time, context data should be marked as unsupported and not generate an exception in the log
_fixture.AddCommand($"LoggingTester CreateSingleLogMessage {InfoMessage} INFO {context}");
_fixture.AddCommand($"LoggingTester CreateSingleLogMessage {InfoMessage} {context}");

_fixture.AddActions
(
Expand Down Expand Up @@ -105,4 +105,4 @@ public ContextDataNotSupportedNetCoreOldestTests(ConsoleDynamicMethodFixtureCore
: base(fixture, output, LoggingFramework.DummyMEL)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public ContextDataTestsBase(TFixture fixture, ITestOutputHelper output, bool tes
_loggingFrameworks.ForEach(x => _fixture.AddCommand($"LoggingTester SetFramework {x} {RandomPortGenerator.NextPort()}"));
_fixture.AddCommand($"LoggingTester Configure");

_loggingFrameworks.ForEach(x => _fixture.AddCommand($"LoggingTester CreateSingleLogMessage {x} {InfoMessage} INFO {FlattenExpectedAttributes(GetExpectedAttributes(x))}"));
_loggingFrameworks.ForEach(x => _fixture.AddCommand($"LoggingTester CreateSingleLogMessageWithContext {x} {InfoMessage} {FlattenExpectedAttributes(GetExpectedAttributes(x))}"));

if (_testNestedContexts) // on supported frameworks, ensure that we don't blow up when accumulating the context key/value pairs
_fixture.AddCommand($"LoggingTester LogMessageInNestedScopes");
Expand Down Expand Up @@ -368,4 +368,4 @@ public NELContextDataCoreLatestTests(ConsoleDynamicMethodFixtureCoreLatest fixtu
}
}

#endregion // NEL
#endregion // NEL
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public LocalDecorationTestsBase(TFixture fixture, ITestOutputHelper output, bool
_fixture.AddCommand($"LoggingTester Configure{layoutType}LayoutAppenderForDecoration");
if (logWithParam)
{
_fixture.AddCommand($"LoggingTester CreateSingleLogMessageInTransactionWithParam {_testMessage}{"{@param}"}");
_fixture.AddCommand($"LoggingTester CreateSingleLogMessageInTransactionWithObjectParameter {_testMessage}{"{@param}"}");
}
else
{
Expand Down Expand Up @@ -743,4 +743,4 @@ public NLogPatternLayoutDecorationDisabledTestsNetCoreOldestTests(ConsoleDynamic

#endregion

#endregion
#endregion
Loading
Loading