Skip to content

Commit

Permalink
feat: Add support for using Sitecore.Logging and log4net together (#2537
Browse files Browse the repository at this point in the history
)
  • Loading branch information
chynesNR committed Jun 11, 2024
1 parent cfb2c28 commit 332529b
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ SPDX-License-Identifier: Apache-2.0
</match>
</tracerFactory>
<!-- Sitecore uses an old fork of log4net with the same namespace -->
<tracerFactory name="log4net">
<tracerFactory name="SitecoreLogging">
<match assemblyName="Sitecore.Logging" className="log4net.Repository.Hierarchy.Logger">
<exactMethodMatcher methodName="CallAppenders" />
</match>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
Expand All @@ -22,11 +22,6 @@ public class Log4netWrapper : IWrapper
private static Func<object, IDictionary> _getGetProperties; // calls GetProperties method
private static Func<object, IDictionary> _getProperties; // getter for Properties property

private static Func<object, object> _getLegacyProperties; // getter for legacy Properties property
private static Func<object, Hashtable> _getLegacyHashtable; // getter for Properties hashtable property

private static bool _legacyVersion = false;

public bool IsTransactionRequired => false;


Expand Down Expand Up @@ -59,17 +54,7 @@ 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");

if (_getLogException == null)
{
if (!VisibilityBypasser.Instance.TryGeneratePropertyAccessor<Exception>(logEventType, "ExceptionObject", out _getLogException))
{
// Legacy property, mainly used by Sitecore
if (!VisibilityBypasser.Instance.TryGeneratePropertyAccessor<Exception>(logEventType, "m_thrownException", out _getLogException))
{
_getLogException = (x) => null;
}
}
}
_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();
Expand Down Expand Up @@ -102,36 +87,9 @@ private void DecorateLogMessage(object logEvent, Type logEventType, IAgent agent
private Dictionary<string, object> GetContextData(object logEvent)
{
var logEventType = logEvent.GetType();

if (_getGetProperties == null && !VisibilityBypasser.Instance.TryGenerateParameterlessMethodCaller(logEventType.Assembly.ToString(), logEventType.FullName, "GetProperties", out _getGetProperties))
{
// Legacy property, mainly used by Sitecore
if (VisibilityBypasser.Instance.TryGeneratePropertyAccessor(logEventType, "MappedContext", out _getGetProperties))
_legacyVersion = true;
else
_getGetProperties = (x) => null;
}
_getGetProperties ??= VisibilityBypasser.Instance.GenerateParameterlessMethodCaller<IDictionary>(logEventType.Assembly.ToString(), logEventType.FullName, "GetProperties");

var contextData = new Dictionary<string, object>();
// In older versions of log4net, there may be additional properties
if (_legacyVersion)
{
// Properties is a "PropertiesCollection", an internal type
var getLegacyProperties = _getLegacyProperties ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(logEventType, "Properties");
var legacyProperties = getLegacyProperties(logEvent);

// PropertyCollection has an internal hashtable that stores the data. The only public method for
// retrieving the data is the indexer [] which is more of a pain to get via reflection.
var propertyCollectionType = legacyProperties.GetType();
var getHashtable = _getLegacyHashtable ??= VisibilityBypasser.Instance.GenerateFieldReadAccessor<Hashtable>(propertyCollectionType.Assembly.ToString(), propertyCollectionType.FullName, "m_ht");

var hashtable = getHashtable(legacyProperties);

foreach (var key in hashtable.Keys)
{
contextData.Add(key.ToString(), hashtable[key]);
}
}

var propertiesDictionary = _getGetProperties(logEvent);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NewRelic.Agent.Api;
using NewRelic.Agent.Api.Experimental;
using NewRelic.Agent.Extensions.Logging;
using NewRelic.Agent.Extensions.Providers.Wrapper;
using NewRelic.Reflection;

namespace NewRelic.Providers.Wrapper.Logging
{
public class SitecoreLoggingWrapper : 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> _getGetProperties; // calls GetProperties method
private static Func<object, IDictionary> _getProperties; // getter for Properties property

private static Func<object, object> _getLegacyProperties; // getter for legacy Properties property
private static Func<object, Hashtable> _getLegacyHashtable; // getter for Properties hashtable property

public bool IsTransactionRequired => false;


private const string WrapperName = "SitecoreLogging";

public CanWrapResponse CanWrap(InstrumentedMethodInfo methodInfo)
{
return new CanWrapResponse(WrapperName.Equals(methodInfo.RequestedWrapperName));
}

public AfterWrappedMethodDelegate BeforeWrappedMethod(InstrumentedMethodCall instrumentedMethodCall, IAgent agent, ITransaction transaction)
{
var logEvent = instrumentedMethodCall.MethodCall.MethodArguments[0];
var logEventType = logEvent.GetType();

RecordLogMessage(logEvent, logEventType, agent);

DecorateLogMessage(logEvent, logEventType, agent);

return Delegates.NoOp;
}

private void RecordLogMessage(object logEvent, Type logEventType, IAgent agent)
{
var getLevelFunc = _getLevel ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(logEventType, "Level");

// RenderedMessage is get only
var getRenderedMessageFunc = _getRenderedMessage ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<string>(logEventType, "RenderedMessage");

// Older versions of log4net only allow access to a timestamp in local time
var getTimestampFunc = _getTimestamp ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<DateTime>(logEventType, "TimeStamp");

_getLogException ??= VisibilityBypasser.Instance.GenerateFieldReadAccessor<Exception>(logEventType, "m_thrownException");

// 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, _getLogException, GetContextData, agent.TraceMetadata.SpanId, agent.TraceMetadata.TraceId);
}

private void DecorateLogMessage(object logEvent, Type logEventType, IAgent agent)
{
if (!agent.Configuration.LogDecoratorEnabled)
{
return;
}

var getProperties = _getProperties ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<IDictionary>(logEventType, "Properties");
var propertiesDictionary = getProperties(logEvent);

if (propertiesDictionary == null)
{
return;
}

// uses the foratted metadata to make a single entry
var formattedMetadata = LoggingHelpers.GetFormattedLinkingMetadata(agent);

// uses underscores to support other frameworks that do not allow hyphens (Serilog)
propertiesDictionary["NR_LINKING"] = formattedMetadata;
}

private Dictionary<string, object> GetContextData(object logEvent)
{
var logEventType = logEvent.GetType();
_getGetProperties ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<IDictionary>(logEventType, "MappedContext");

var contextData = new Dictionary<string, object>();
// In older versions of log4net, there may be additional properties

// Properties is a "PropertiesCollection", an internal type
var getLegacyProperties = _getLegacyProperties ??= VisibilityBypasser.Instance.GeneratePropertyAccessor<object>(logEventType, "Properties");
var legacyProperties = getLegacyProperties(logEvent);

// PropertyCollection has an internal hashtable that stores the data. The only public method for
// retrieving the data is the indexer [] which is more of a pain to get via reflection.
var propertyCollectionType = legacyProperties.GetType();
var getHashtable = _getLegacyHashtable ??= VisibilityBypasser.Instance.GenerateFieldReadAccessor<Hashtable>(propertyCollectionType.Assembly.ToString(), propertyCollectionType.FullName, "m_ht");

var hashtable = getHashtable(legacyProperties);

foreach (var key in hashtable.Keys)
{
contextData.Add(key.ToString(), hashtable[key]);
}

var propertiesDictionary = _getGetProperties(logEvent);

if (propertiesDictionary != null && propertiesDictionary.Count > 0)
{
foreach (var key in propertiesDictionary.Keys)
{
contextData.Add(key.ToString(), propertiesDictionary[key]);
}
}

return contextData.Any() ? contextData : null;
}
}
}
Loading

0 comments on commit 332529b

Please sign in to comment.