将 Sentry.NET 与 log4net 结合使用
Using Sentry.NET in conjunction with log4net
我有一个关于将 Sentry.NET 与 log4net 结合使用的问题。
我安装了Sentry.Log4Net NuGet package and setup the config file according to this example
总的来说,它是有效的。根据我在配置文件中配置的调试级别,Log4net 调用会报告给我们的 Sentry 帐户。
但是,每个 Log 调用都会创建自己的 Sentry Event。我本以为 Log4net 调用会创建 Sentry "Breadcrumbs"。
将数百个日志事件报告给哨兵后端并没有多大用处。
有没有办法改变这种行为?
您说得对,此时 Sentry log4net 仅引发事件。如果可能,我建议更改为支持结构化日志记录的日志库之一,例如 Serilog。
其他日志记录集成有这样的行为,您可以配置两个日志级别。一个用于设置创建面包屑的最低级别,另一个用于发送事件。
您可以在 GitHub 上提出问题请求此功能。甚至,通过拉取请求做出贡献以更改 SentryAppender to behave like the SerilogSink, NLog Target or the Microsoft.Extensions.Logging integration.
最终创建了我自己的 SentryAppender,并提供了 MinimumBreadcrumbLevel
.
的选项
SentryAppender 实现:
public class SentryAppender : AppenderSkeleton
{
private readonly Func<string, IDisposable> _initAction;
private volatile IDisposable _sdkHandle;
private readonly object _initSync = new object();
internal static readonly SdkVersion NameAndVersion
= typeof(SentryAppender).Assembly.GetNameAndVersion();
private static readonly string ProtocolPackageName = NameAndVersion.Name;
internal IHub Hub { get; set; }
/// <summary>
/// Sentry DSN.
/// </summary>
public string Dsn { get; set; }
/// <summary>
/// Whether to send the Identity or not.
/// </summary>
public bool SendIdentity { get; set; }
/// <summary>
/// Environment to send in the event.
/// </summary>
public string Environment { get; set; }
/// <summary>
/// Minimum log level to be included in the breadcrumb.
/// </summary>
public Level MinimumBreadcrumbLevel { get; set; }
/// <summary>
/// Creates a new instance of the <see cref="SentryAppender"/>.
/// </summary>
public SentryAppender() : this(SentrySdk.Init, HubAdapter.Instance)
{ }
internal SentryAppender(
Func<string, IDisposable> initAction,
IHub hubGetter)
{
Debug.Assert(initAction != null);
Debug.Assert(hubGetter != null);
_initAction = initAction;
Hub = hubGetter;
}
/// <summary>
/// Append log.
/// </summary>
/// <param name="loggingEvent">The event.</param>
protected override void Append(LoggingEvent loggingEvent)
{
if (loggingEvent == null)
{
return;
}
if (!Hub.IsEnabled && _sdkHandle == null)
{
if (Dsn == null)
{
return;
}
lock (_initSync)
{
if (_sdkHandle == null)
{
_sdkHandle = _initAction(Dsn);
Debug.Assert(_sdkHandle != null);
}
}
}
// Either log a BreadCrumb or an Event
if (MinimumBreadcrumbLevel < loggingEvent.Level)
{
var exception = loggingEvent.ExceptionObject ?? loggingEvent.MessageObject as Exception;
var evt = new SentryEvent(exception)
{
Sdk =
{
Name = Constants.SdkName,
Version = NameAndVersion.Version
},
Logger = loggingEvent.LoggerName,
Level = loggingEvent.ToSentryLevel()
};
evt.Sdk.AddPackage(ProtocolPackageName, NameAndVersion.Version);
if (!string.IsNullOrWhiteSpace(loggingEvent.RenderedMessage))
{
evt.Message = loggingEvent.RenderedMessage;
}
evt.SetExtras(GetLoggingEventProperties(loggingEvent));
if (SendIdentity && !string.IsNullOrEmpty(loggingEvent.Identity))
{
evt.User = new User
{
Id = loggingEvent.Identity
};
}
if (!string.IsNullOrWhiteSpace(Environment))
{
evt.Environment = Environment;
}
Hub.CaptureEvent(evt);
}
else
{
string message = !string.IsNullOrWhiteSpace(loggingEvent.RenderedMessage) ? loggingEvent.RenderedMessage : "";
string type = "";
string category = loggingEvent.LoggerName;
BreadcrumbLevel level = loggingEvent.ToBreadcrumbLevel();
IDictionary<string, string> data = GetLoggingEventProperties(loggingEvent).ToDictionary(x => x.Key, x => x.Value.ToString());
Hub.AddBreadcrumb(message, category, type, data, level);
}
}
private static IEnumerable<KeyValuePair<string, object>> GetLoggingEventProperties(LoggingEvent loggingEvent)
{
var properties = loggingEvent.GetProperties();
if (properties == null)
{
yield break;
}
foreach (var key in properties.GetKeys())
{
if (!string.IsNullOrWhiteSpace(key)
&& !key.StartsWith("log4net:", StringComparison.OrdinalIgnoreCase))
{
var value = properties[key];
if (value != null
&& (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)))
{
yield return new KeyValuePair<string, object>(key, value);
}
}
}
var locInfo = loggingEvent.LocationInformation;
if (locInfo != null)
{
if (!string.IsNullOrEmpty(locInfo.ClassName))
{
yield return new KeyValuePair<string, object>(nameof(locInfo.ClassName), locInfo.ClassName);
}
if (!string.IsNullOrEmpty(locInfo.FileName))
{
yield return new KeyValuePair<string, object>(nameof(locInfo.FileName), locInfo.FileName);
}
if (int.TryParse(locInfo.LineNumber, out var lineNumber) && lineNumber != 0)
{
yield return new KeyValuePair<string, object>(nameof(locInfo.LineNumber), lineNumber);
}
if (!string.IsNullOrEmpty(locInfo.MethodName))
{
yield return new KeyValuePair<string, object>(nameof(locInfo.MethodName), locInfo.MethodName);
}
}
if (!string.IsNullOrEmpty(loggingEvent.ThreadName))
{
yield return new KeyValuePair<string, object>(nameof(loggingEvent.ThreadName), loggingEvent.ThreadName);
}
if (!string.IsNullOrEmpty(loggingEvent.Domain))
{
yield return new KeyValuePair<string, object>(nameof(loggingEvent.Domain), loggingEvent.Domain);
}
if (loggingEvent.Level != null)
{
yield return new KeyValuePair<string, object>("log4net-level", loggingEvent.Level.Name);
}
}
/// <summary>
/// Disposes the SDK if initialized.
/// </summary>
protected override void OnClose()
{
base.OnClose();
_sdkHandle?.Dispose();
}
}
用于映射不同日志级别的助手 class:
internal static class LevelMapping
{
public static SentryLevel? ToSentryLevel(this LoggingEvent loggingLevel)
{
switch (loggingLevel.Level)
{
case var l when l == Level.Fatal
|| l == Level.Emergency
|| l == Level.All:
return SentryLevel.Fatal;
case var l when l == Level.Alert
|| l == Level.Critical
|| l == Level.Severe
|| l == Level.Error:
return SentryLevel.Error;
case var l when l == Level.Warn:
return SentryLevel.Warning;
case var l when l == Level.Notice
|| l == Level.Info:
return SentryLevel.Info;
case var l when l == Level.Debug
|| l == Level.Verbose
|| l == Level.Trace
|| l == Level.Finer
|| l == Level.Finest
|| l == Level.Fine:
return SentryLevel.Debug;
}
return null;
}
public static BreadcrumbLevel ToBreadcrumbLevel(this LoggingEvent loggingLevel) {
switch (loggingLevel.Level) {
case var l when l == Level.Fatal
|| l == Level.Emergency
|| l == Level.All:
return BreadcrumbLevel.Critical;
case var l when l == Level.Alert
|| l == Level.Critical
|| l == Level.Severe
|| l == Level.Error:
return BreadcrumbLevel.Error;
case var l when l == Level.Warn:
return BreadcrumbLevel.Warning;
case var l when l == Level.Notice
|| l == Level.Info:
return BreadcrumbLevel.Info;
case var l when l == Level.Debug
|| l == Level.Verbose
|| l == Level.Trace
|| l == Level.Finer
|| l == Level.Finest
|| l == Level.Fine:
return BreadcrumbLevel.Debug;
}
return BreadcrumbLevel.Debug;
}
}
log4net.config 示例:
<log4net>
<root>
<level value="ALL" />
<appender-ref ref="SentryAppender" />
</root>
<appender name="SentryAppender" type="YourNamespace.SentryAppender">
<!-- If we initialize the SDK via code (SentrySdk.Init), DSN is not required here. -->
<!-- <Dsn value="" /> -->
<!-- Level options: ALL DEBUG INFO WARN ERROR FATAL OFF -->
<threshold value="DEBUG" />
<!-- Everything below or equal to this level will be recorded as a Sentry "breadcrumb" -->
<MinimumBreadcrumbLevel value="WARN" />
</appender>
</log4net>
我有一个关于将 Sentry.NET 与 log4net 结合使用的问题。
我安装了Sentry.Log4Net NuGet package and setup the config file according to this example
总的来说,它是有效的。根据我在配置文件中配置的调试级别,Log4net 调用会报告给我们的 Sentry 帐户。
但是,每个 Log 调用都会创建自己的 Sentry Event。我本以为 Log4net 调用会创建 Sentry "Breadcrumbs"。
将数百个日志事件报告给哨兵后端并没有多大用处。
有没有办法改变这种行为?
您说得对,此时 Sentry log4net 仅引发事件。如果可能,我建议更改为支持结构化日志记录的日志库之一,例如 Serilog。
其他日志记录集成有这样的行为,您可以配置两个日志级别。一个用于设置创建面包屑的最低级别,另一个用于发送事件。
您可以在 GitHub 上提出问题请求此功能。甚至,通过拉取请求做出贡献以更改 SentryAppender to behave like the SerilogSink, NLog Target or the Microsoft.Extensions.Logging integration.
最终创建了我自己的 SentryAppender,并提供了 MinimumBreadcrumbLevel
.
SentryAppender 实现:
public class SentryAppender : AppenderSkeleton
{
private readonly Func<string, IDisposable> _initAction;
private volatile IDisposable _sdkHandle;
private readonly object _initSync = new object();
internal static readonly SdkVersion NameAndVersion
= typeof(SentryAppender).Assembly.GetNameAndVersion();
private static readonly string ProtocolPackageName = NameAndVersion.Name;
internal IHub Hub { get; set; }
/// <summary>
/// Sentry DSN.
/// </summary>
public string Dsn { get; set; }
/// <summary>
/// Whether to send the Identity or not.
/// </summary>
public bool SendIdentity { get; set; }
/// <summary>
/// Environment to send in the event.
/// </summary>
public string Environment { get; set; }
/// <summary>
/// Minimum log level to be included in the breadcrumb.
/// </summary>
public Level MinimumBreadcrumbLevel { get; set; }
/// <summary>
/// Creates a new instance of the <see cref="SentryAppender"/>.
/// </summary>
public SentryAppender() : this(SentrySdk.Init, HubAdapter.Instance)
{ }
internal SentryAppender(
Func<string, IDisposable> initAction,
IHub hubGetter)
{
Debug.Assert(initAction != null);
Debug.Assert(hubGetter != null);
_initAction = initAction;
Hub = hubGetter;
}
/// <summary>
/// Append log.
/// </summary>
/// <param name="loggingEvent">The event.</param>
protected override void Append(LoggingEvent loggingEvent)
{
if (loggingEvent == null)
{
return;
}
if (!Hub.IsEnabled && _sdkHandle == null)
{
if (Dsn == null)
{
return;
}
lock (_initSync)
{
if (_sdkHandle == null)
{
_sdkHandle = _initAction(Dsn);
Debug.Assert(_sdkHandle != null);
}
}
}
// Either log a BreadCrumb or an Event
if (MinimumBreadcrumbLevel < loggingEvent.Level)
{
var exception = loggingEvent.ExceptionObject ?? loggingEvent.MessageObject as Exception;
var evt = new SentryEvent(exception)
{
Sdk =
{
Name = Constants.SdkName,
Version = NameAndVersion.Version
},
Logger = loggingEvent.LoggerName,
Level = loggingEvent.ToSentryLevel()
};
evt.Sdk.AddPackage(ProtocolPackageName, NameAndVersion.Version);
if (!string.IsNullOrWhiteSpace(loggingEvent.RenderedMessage))
{
evt.Message = loggingEvent.RenderedMessage;
}
evt.SetExtras(GetLoggingEventProperties(loggingEvent));
if (SendIdentity && !string.IsNullOrEmpty(loggingEvent.Identity))
{
evt.User = new User
{
Id = loggingEvent.Identity
};
}
if (!string.IsNullOrWhiteSpace(Environment))
{
evt.Environment = Environment;
}
Hub.CaptureEvent(evt);
}
else
{
string message = !string.IsNullOrWhiteSpace(loggingEvent.RenderedMessage) ? loggingEvent.RenderedMessage : "";
string type = "";
string category = loggingEvent.LoggerName;
BreadcrumbLevel level = loggingEvent.ToBreadcrumbLevel();
IDictionary<string, string> data = GetLoggingEventProperties(loggingEvent).ToDictionary(x => x.Key, x => x.Value.ToString());
Hub.AddBreadcrumb(message, category, type, data, level);
}
}
private static IEnumerable<KeyValuePair<string, object>> GetLoggingEventProperties(LoggingEvent loggingEvent)
{
var properties = loggingEvent.GetProperties();
if (properties == null)
{
yield break;
}
foreach (var key in properties.GetKeys())
{
if (!string.IsNullOrWhiteSpace(key)
&& !key.StartsWith("log4net:", StringComparison.OrdinalIgnoreCase))
{
var value = properties[key];
if (value != null
&& (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)))
{
yield return new KeyValuePair<string, object>(key, value);
}
}
}
var locInfo = loggingEvent.LocationInformation;
if (locInfo != null)
{
if (!string.IsNullOrEmpty(locInfo.ClassName))
{
yield return new KeyValuePair<string, object>(nameof(locInfo.ClassName), locInfo.ClassName);
}
if (!string.IsNullOrEmpty(locInfo.FileName))
{
yield return new KeyValuePair<string, object>(nameof(locInfo.FileName), locInfo.FileName);
}
if (int.TryParse(locInfo.LineNumber, out var lineNumber) && lineNumber != 0)
{
yield return new KeyValuePair<string, object>(nameof(locInfo.LineNumber), lineNumber);
}
if (!string.IsNullOrEmpty(locInfo.MethodName))
{
yield return new KeyValuePair<string, object>(nameof(locInfo.MethodName), locInfo.MethodName);
}
}
if (!string.IsNullOrEmpty(loggingEvent.ThreadName))
{
yield return new KeyValuePair<string, object>(nameof(loggingEvent.ThreadName), loggingEvent.ThreadName);
}
if (!string.IsNullOrEmpty(loggingEvent.Domain))
{
yield return new KeyValuePair<string, object>(nameof(loggingEvent.Domain), loggingEvent.Domain);
}
if (loggingEvent.Level != null)
{
yield return new KeyValuePair<string, object>("log4net-level", loggingEvent.Level.Name);
}
}
/// <summary>
/// Disposes the SDK if initialized.
/// </summary>
protected override void OnClose()
{
base.OnClose();
_sdkHandle?.Dispose();
}
}
用于映射不同日志级别的助手 class:
internal static class LevelMapping
{
public static SentryLevel? ToSentryLevel(this LoggingEvent loggingLevel)
{
switch (loggingLevel.Level)
{
case var l when l == Level.Fatal
|| l == Level.Emergency
|| l == Level.All:
return SentryLevel.Fatal;
case var l when l == Level.Alert
|| l == Level.Critical
|| l == Level.Severe
|| l == Level.Error:
return SentryLevel.Error;
case var l when l == Level.Warn:
return SentryLevel.Warning;
case var l when l == Level.Notice
|| l == Level.Info:
return SentryLevel.Info;
case var l when l == Level.Debug
|| l == Level.Verbose
|| l == Level.Trace
|| l == Level.Finer
|| l == Level.Finest
|| l == Level.Fine:
return SentryLevel.Debug;
}
return null;
}
public static BreadcrumbLevel ToBreadcrumbLevel(this LoggingEvent loggingLevel) {
switch (loggingLevel.Level) {
case var l when l == Level.Fatal
|| l == Level.Emergency
|| l == Level.All:
return BreadcrumbLevel.Critical;
case var l when l == Level.Alert
|| l == Level.Critical
|| l == Level.Severe
|| l == Level.Error:
return BreadcrumbLevel.Error;
case var l when l == Level.Warn:
return BreadcrumbLevel.Warning;
case var l when l == Level.Notice
|| l == Level.Info:
return BreadcrumbLevel.Info;
case var l when l == Level.Debug
|| l == Level.Verbose
|| l == Level.Trace
|| l == Level.Finer
|| l == Level.Finest
|| l == Level.Fine:
return BreadcrumbLevel.Debug;
}
return BreadcrumbLevel.Debug;
}
}
log4net.config 示例:
<log4net>
<root>
<level value="ALL" />
<appender-ref ref="SentryAppender" />
</root>
<appender name="SentryAppender" type="YourNamespace.SentryAppender">
<!-- If we initialize the SDK via code (SentrySdk.Init), DSN is not required here. -->
<!-- <Dsn value="" /> -->
<!-- Level options: ALL DEBUG INFO WARN ERROR FATAL OFF -->
<threshold value="DEBUG" />
<!-- Everything below or equal to this level will be recorded as a Sentry "breadcrumb" -->
<MinimumBreadcrumbLevel value="WARN" />
</appender>
</log4net>