Skip to content

Commit ad24201

Browse files
authored
feat: Add support for Serilog.Extensions.Logging and NLog.Extensions.Logging. (#1860) (#1859)
1 parent f526105 commit ad24201

File tree

19 files changed

+429
-91
lines changed

19 files changed

+429
-91
lines changed

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Logging/LogProviders.cs

+12-13
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,20 @@
66

77
namespace NewRelic.Agent.Extensions.Logging
88
{
9-
public enum LogProvider
10-
{
11-
Log4Net,
12-
Serilog,
13-
NLog
14-
}
15-
169
public static class LogProviders
1710
{
18-
public static readonly bool[] RegisteredLogProvider = new bool[Enum.GetNames(typeof(LogProvider)).Length];
19-
20-
public static readonly List<string> Log4NetProviderNames = new List<string> { "Microsoft.Extensions.Logging.Log4NetProvider", "log4net.Extensions.Logging.Log4NetProvider" };
21-
22-
public static readonly List<string> SerilogProviderNames = new List<string> { "Microsoft.Extensions.Logging.SerilogLoggerProvider", "Serilog.Extensions.Logging.SerilogLoggerProvider" };
11+
// This will only be set once, when a Microsoft.Extensions.Logging registration method is called.
12+
// It can safely be read concurrently.
13+
public static bool KnownMELProviderEnabled = false;
2314

24-
public static readonly List<string> NLogProviderNames = new List<string> { "Microsoft.Extensions.Logging.NLogLoggerProvider", "NLog.Extensions.Logging.NLogLoggerProvider" };
15+
public static readonly List<string> KnownMELProviders = new List<string>
16+
{
17+
"Microsoft.Extensions.Logging.Log4NetProvider",
18+
"log4net.Extensions.Logging.Log4NetProvider",
19+
"Microsoft.Extensions.Logging.SerilogLoggerProvider",
20+
"Serilog.Extensions.Logging.SerilogLoggerProvider",
21+
"Microsoft.Extensions.Logging.NLogLoggerProvider",
22+
"NLog.Extensions.Logging.NLogLoggerProvider"
23+
};
2524
}
2625
}

src/Agent/NewRelic/Agent/Extensions/NewRelic.Agent.Extensions/Reflection/VisibilityBypasser.cs

+12
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,18 @@ private static MethodInfo GetMethodInfo(Type type, string methodName)
607607
return methodInfo;
608608
}
609609

610+
public Func<TResult> GenerateParameterlessStaticMethodCaller<TResult>(string assemblyName, string typeName, string methodName)
611+
{
612+
var ownerType = GetType(assemblyName, typeName);
613+
614+
var methodInfo = ownerType.GetMethod(methodName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
615+
if (methodInfo == null)
616+
{
617+
throw new KeyNotFoundException(string.Format("Unable to find method {0} in type {1}", methodName, ownerType.AssemblyQualifiedName));
618+
}
619+
return (Func<TResult>)methodInfo.CreateDelegate(typeof(Func<TResult>));
620+
}
621+
610622
private static PropertyInfo GetPropertyInfo(Type type, string propertyName)
611623
{
612624
var propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/Log4NetLogging/Log4netWrapper.cs

+1-6
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,7 @@ public class Log4netWrapper : IWrapper
3434

3535
public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
3636
{
37-
if (!LogProviders.RegisteredLogProvider[(int)LogProvider.Log4Net])
38-
{
39-
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
40-
}
41-
42-
return new CanWrapResponse(false);
37+
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
4338
}
4439

4540
public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/MicrosoftExtensionsLogging/AddProviderRegistrationWrapper.cs

+5-11
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,14 @@ public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
2222
public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
2323
{
2424
var provider = instrumentedMethodCall.MethodCall.MethodArguments[0].ToString();
25-
if (LogProviders.Log4NetProviderNames.Contains(provider))
25+
if (LogProviders.KnownMELProviders.Contains(provider))
2626
{
27-
LogProviders.RegisteredLogProvider[(int)LogProvider.Log4Net] = true;
28-
agent.Logger.Log(Level.Info, "Detected log4net provider in use with Microsoft.Extensions.Logging, disabling log4net instrumentation.");
27+
LogProviders.KnownMELProviderEnabled = true;
28+
agent.Logger.Log(Level.Info, $"Known log provider {provider} in use. Disabling Microsoft.Extensions.Logging instrumentation.");
2929
}
30-
else if (LogProviders.SerilogProviderNames.Contains(provider))
30+
else
3131
{
32-
LogProviders.RegisteredLogProvider[(int)LogProvider.Serilog] = true;
33-
agent.Logger.Log(Level.Info, "Detected Serilog provider in use with Microsoft.Extensions.Logging, disabling Serilog instrumentation.");
34-
}
35-
else if (LogProviders.NLogProviderNames.Contains(provider))
36-
{
37-
LogProviders.RegisteredLogProvider[(int)LogProvider.NLog] = true;
38-
agent.Logger.Log(Level.Info, "Detected NLog provider in use with Microsoft.Extensions.Logging, disabling NLog instrumentation.");
32+
agent.Logger.Log(Level.Info, $"Log provider {provider} will use Microsoft.Extensions.Logging instrumentation.");
3933
}
4034

4135
return Delegates.NoOp;

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/MicrosoftExtensionsLogging/MicrosoftLoggingWrapper.cs

+18-23
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99
using NewRelic.Agent.Extensions.Providers.Wrapper;
1010
using System.Collections.Generic;
1111
using NewRelic.Reflection;
12-
using System.Dynamic;
12+
using Microsoft.Extensions.Logging;
13+
using System.Reflection;
1314

1415
namespace NewRelic.Providers.Wrapper.MicrosoftExtensionsLogging
1516
{
1617
public class MicrosoftLoggingWrapper : IWrapper
1718
{
1819
// Cached accessor functions
1920
private static Func<object, dynamic> _getLoggersArray;
20-
private static Func<object, object> _getLoggerProperty;
21-
private static Func<object, MEL.IExternalScopeProvider> _getScopeProvider;
21+
private static PropertyInfo _scopeProviderPropertyInfo;
2222

2323
private static bool _contextDataNotSupported = false;
2424

@@ -28,7 +28,11 @@ public class MicrosoftLoggingWrapper : IWrapper
2828

2929
public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
3030
{
31-
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
31+
if (!LogProviders.KnownMELProviderEnabled)
32+
{
33+
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
34+
}
35+
return new CanWrapResponse(false);
3236
}
3337

3438
public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
@@ -71,27 +75,24 @@ private static Dictionary<string, object> GetContextData(MEL.ILogger logger, IAg
7175

7276
try
7377
{
74-
// We are trying to access this property:
75-
// logger.Loggers[0].Logger.ScopeProvider
78+
// MEL keeps an array of scope handlers. We can ask one of them for the current scope data.
7679

77-
// Get the array of Loggers (logger.Loggers[])
78-
var getLoggersArrayFunc = _getLoggersArray ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<dynamic>(logger.GetType(), "Loggers");
80+
// Get the array of ScopeLoggers (logger.ScopeLoggers[])
81+
var getLoggersArrayFunc = _getLoggersArray ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<dynamic>(logger.GetType(), "ScopeLoggers");
7982
var loggers = getLoggersArrayFunc(logger);
8083

81-
// Get the first logger in the array (logger.Loggers[0])
84+
// Get the first ScopeLogger in the array (logger.ScopeLoggers[0])
85+
// If there is more than one scope logger, they've all received the same data, so the first
86+
// one should be fine
8287
object firstLogger = loggers.GetValue(0);
8388

84-
// Get the internal logger (logger.Loggers[0].Logger)
85-
var getLoggerPropertyFunc = _getLoggerProperty ??= firstLogger.GetType().GetProperty("Logger").GetValue;
86-
object internalLogger = getLoggerPropertyFunc(firstLogger);
87-
88-
// Get the scope provider from that logger (logger.Loggers[0].Logger.ScopeProvider)
89-
var getScopeProviderFunc = _getScopeProvider ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<MEL.IExternalScopeProvider>(internalLogger.GetType(), "ScopeProvider");
90-
var scopeProvider = getScopeProviderFunc(internalLogger);
89+
// Get the scope provider from that logger (logger.ScopeLoggers[0].ExternalScopeProvider)
90+
var scopeProviderPI = _scopeProviderPropertyInfo ??= firstLogger.GetType().GetProperty("ExternalScopeProvider");
91+
var scopeProvider = scopeProviderPI.GetValue(firstLogger) as IExternalScopeProvider;
9192

9293
// Get the context data
9394
var harvestedKvps = new Dictionary<string, object>();
94-
scopeProvider.ForEachScope((scopeObject, accumulatedKvps) =>
95+
scopeProvider?.ForEachScope((scopeObject, accumulatedKvps) =>
9596
{
9697
if (scopeObject is IEnumerable<KeyValuePair<string, object>> kvps)
9798
{
@@ -124,12 +125,6 @@ private AfterWrappedMethodDelegate DecorateLogMessage(MEL.ILogger logger, IAgent
124125
return Delegates.NoOp;
125126
}
126127

127-
// NLog can alter the message so we want to skip MEL decoration for NLog
128-
if (LogProviders.RegisteredLogProvider[(int)LogProvider.NLog])
129-
{
130-
return Delegates.NoOp;
131-
}
132-
133128
// uses the formatted metadata to make a single entry
134129
var formattedMetadata = LoggingHelpers.GetFormattedLinkingMetadata(agent);
135130

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/NLogLogging/NLogWrapper.cs

+20-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Xml.Linq;
67
using NewRelic.Agent.Api;
78
using NewRelic.Agent.Api.Experimental;
89
using NewRelic.Agent.Extensions.Logging;
@@ -20,6 +21,7 @@ public class NLogWrapper : IWrapper
2021
private static Func<object, DateTime> _getTimestamp;
2122
private static Func<object, Exception> _getLogException;
2223
private static Func<object, IDictionary<object, object>> _getPropertiesDictionary;
24+
private static Func<IEnumerable<KeyValuePair<string, object>>> _getScopeData;
2325

2426
public bool IsTransactionRequired => false;
2527

@@ -38,12 +40,7 @@ public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall ins
3840
var logEvent = instrumentedMethodCall.MethodCall.MethodArguments[2];
3941
var logEventType = logEvent.GetType();
4042

41-
if (!LogProviders.RegisteredLogProvider[(int)LogProvider.NLog])
42-
{
43-
RecordLogMessage(logEvent, logEventType, agent);
44-
}
45-
46-
// We want this to happen instead of MEL so no provider check here.
43+
RecordLogMessage(logEvent, logEventType, agent);
4744
DecorateLogMessage(logEvent, logEventType, agent);
4845

4946
return Delegates.NoOp;
@@ -127,6 +124,23 @@ private Dictionary<string, object> GetContextData(object logEvent)
127124
contextData[property.Key.ToString()] = property.Value;
128125
}
129126

127+
// NLog treats and stores Properties and Scope Context differently. If we need to add support for older versions of NLog, it's two calls instead of GetAllProperties():
128+
// NLog.MappedDiagnosticsLogicalContext.GetNames()
129+
// NLog.MappedDiagnosticsLogicalContext.Get(name)
130+
try
131+
{
132+
_getScopeData = _getScopeData ??= VisibilityBypasser.Instance.GenerateParameterlessStaticMethodCaller<IEnumerable<KeyValuePair<string, object>>>("NLog", "NLog.ScopeContext", "GetAllProperties");
133+
}
134+
catch
135+
{
136+
_getScopeData = () => new List<KeyValuePair<string, object>>();
137+
}
138+
var scopeData = _getScopeData();
139+
foreach (var pair in scopeData)
140+
{
141+
contextData[pair.Key] = pair.Value;
142+
}
143+
130144
return contextData;
131145
}
132146
}

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/SerilogLogging/NewRelicSerilogSink.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@ public NewRelicSerilogSink(IAgent agent)
2323

2424
public void Emit(LogEvent logEvent)
2525
{
26-
//This check is to prevent forwarding duplicate logs when Microsoft.Extensions.Logging is used.
27-
if (!LogProviders.RegisteredLogProvider[(int)LogProvider.Serilog])
28-
{
29-
RecordLogMessage(logEvent);
30-
}
26+
RecordLogMessage(logEvent);
3127
}
3228

3329
private void RecordLogMessage(LogEvent logEvent)

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/SerilogLogging/SerilogCreateLoggerWrapper.cs

+1-6
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,7 @@ public class SerilogCreateLoggerWrapper : IWrapper
1616

1717
public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
1818
{
19-
if (!LogProviders.RegisteredLogProvider[(int)LogProvider.Serilog])
20-
{
21-
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
22-
}
23-
24-
return new CanWrapResponse(false);
19+
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
2520
}
2621

2722
public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)

src/Agent/NewRelic/Agent/Extensions/Providers/Wrapper/SerilogLogging/SerilogDispatchWrapper.cs

+1-6
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,7 @@ public class SerilogDispatchWrapper : IWrapper
2525

2626
public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
2727
{
28-
if (!LogProviders.RegisteredLogProvider[(int)LogProvider.Serilog])
29-
{
30-
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
31-
}
32-
33-
return new CanWrapResponse(false);
28+
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
3429
}
3530

3631
public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)

tests/Agent/IntegrationTests/IntegrationTestHelpers/AgentLogBase.cs

-3
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,6 @@ public abstract class AgentLogBase
6969
public const string SpanStreamingSuccessfullyProcessedByServerResponseLogLineRegex = FinestLogLinePrefixRegex + @"SpanStreamingService: consumer \d+ - Received gRPC Server response messages: (\d+)";
7070
public const string SpanStreamingResponseGrpcError = FinestLogLinePrefixRegex + @"ResponseStreamWrapper: consumer \d+ - gRPC RpcException encountered while handling gRPC server responses: (.*)";
7171

72-
// ContextData related messages
73-
public const string ContextDataNotSupportedLogLineRegex = WarnLogLinePrefixRegex + @".* Context data is not supported for this logging framework.";
74-
7572
public AgentLogBase(RemoteApplication remoteApplication)
7673
{
7774
_remoteApplication = remoteApplication;

tests/Agent/IntegrationTests/IntegrationTests/Logging/Common.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ public enum LoggingFramework
1111
SerilogWeb,
1212
NLog,
1313
DummyMEL,
14-
Sitecore
14+
Sitecore,
15+
SerilogEL,
16+
NLogEL
1517
}
1618

1719
public class LogUtils
@@ -28,6 +30,7 @@ public static string GetFrameworkName(LoggingFramework loggingFramework)
2830
case LoggingFramework.Serilog:
2931
return "serilog";
3032
case LoggingFramework.NLog:
33+
case LoggingFramework.NLogEL:
3134
return "nlog";
3235
case LoggingFramework.DummyMEL:
3336
return "DummyMEL";
@@ -73,6 +76,7 @@ public static string GetLevelName(LoggingFramework loggingFramework, string leve
7376
}
7477
case LoggingFramework.SerilogWeb:
7578
case LoggingFramework.Serilog:
79+
case LoggingFramework.SerilogEL:
7680
switch (level)
7781
{
7882
case "DEBUG":
@@ -91,6 +95,7 @@ public static string GetLevelName(LoggingFramework loggingFramework, string leve
9195
return level;
9296
}
9397
case LoggingFramework.NLog:
98+
case LoggingFramework.NLogEL:
9499
switch (level)
95100
{
96101
case "NOMESSAGE":

tests/Agent/IntegrationTests/IntegrationTests/Logging/ContextDataNotSupportedTests.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,7 @@ public ContextDataNotSupportedTestsBase(TFixture fixture, ITestOutputHelper outp
7070
[Fact]
7171
public void Test()
7272
{
73-
// verify the "not supported" warning was logged
74-
var match = _fixture.AgentLog.TryGetLogLines(AgentLogBase.ContextDataNotSupportedLogLineRegex);
75-
Assert.Single(match);
76-
77-
// verify the log data was forwarded, but *without* the attributes
73+
// verify the log data was forwarded, but *without* the attributes (since our dummy logger doesn't support them)
7874
var expectedLogLines = new[]
7975
{
8076
new Assertions.ExpectedLogLine

tests/Agent/IntegrationTests/IntegrationTests/Logging/ContextDataTests.cs

+54
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,58 @@ public SitecoreContextDataFW48Tests(ConsoleDynamicMethodFixtureFW48 fixture, ITe
279279
}
280280

281281
#endregion // Sitecore
282+
283+
#region SEL
284+
public class SELContextDataFWLatestTests : ContextDataTestsBase<ConsoleDynamicMethodFixtureFWLatest>
285+
{
286+
public SELContextDataFWLatestTests(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output)
287+
: base(fixture, output, LoggingFramework.SerilogEL)
288+
{
289+
}
290+
}
291+
292+
public class SELContextDataFW48Tests : ContextDataTestsBase<ConsoleDynamicMethodFixtureFW48>
293+
{
294+
public SELContextDataFW48Tests(ConsoleDynamicMethodFixtureFW48 fixture, ITestOutputHelper output)
295+
: base(fixture, output, LoggingFramework.SerilogEL)
296+
{
297+
}
298+
}
299+
300+
public class SELContextDataCoreLatestTests : ContextDataTestsBase<ConsoleDynamicMethodFixtureCoreLatest>
301+
{
302+
public SELContextDataCoreLatestTests(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output)
303+
: base(fixture, output, LoggingFramework.SerilogEL)
304+
{
305+
}
306+
}
307+
308+
public class SELContextDataCoreOldestTests : ContextDataTestsBase<ConsoleDynamicMethodFixtureCoreOldest>
309+
{
310+
public SELContextDataCoreOldestTests(ConsoleDynamicMethodFixtureCoreOldest fixture, ITestOutputHelper output)
311+
: base(fixture, output, LoggingFramework.SerilogEL)
312+
{
313+
}
314+
}
315+
316+
#endregion // SEL
317+
318+
#region NEL
319+
public class NELContextDataFWLatestTests : ContextDataTestsBase<ConsoleDynamicMethodFixtureFWLatest>
320+
{
321+
public NELContextDataFWLatestTests(ConsoleDynamicMethodFixtureFWLatest fixture, ITestOutputHelper output)
322+
: base(fixture, output, LoggingFramework.NLogEL)
323+
{
324+
}
325+
}
326+
327+
public class NELContextDataCoreLatestTests : ContextDataTestsBase<ConsoleDynamicMethodFixtureCoreLatest>
328+
{
329+
public NELContextDataCoreLatestTests(ConsoleDynamicMethodFixtureCoreLatest fixture, ITestOutputHelper output)
330+
: base(fixture, output, LoggingFramework.NLogEL)
331+
{
332+
}
333+
}
334+
335+
#endregion // NEL
282336
}

0 commit comments

Comments
 (0)