Skip to content
Open
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
1 change: 1 addition & 0 deletions .craft.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ targets:
nuget:Sentry.Hangfire:
nuget:Sentry.Log4Net:
nuget:Sentry.Maui:
nuget:Sentry.Maui.CommunityToolkit.Mvvm:
nuget:Sentry.NLog:
nuget:Sentry.OpenTelemetry:
nuget:Sentry.OpenTelemetry.Exporter:
Expand Down
18 changes: 2 additions & 16 deletions src/Sentry.Extensions.Logging/SentryLogger.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Sentry.Infrastructure;
using Sentry.Internal;

namespace Sentry.Extensions.Logging;

Expand Down Expand Up @@ -182,22 +183,7 @@ private bool ShouldAddBreadcrumb(
exception));


private bool IsFromSentry()
{
if (string.Equals(CategoryName, "Sentry", StringComparison.Ordinal))
{
return true;
}

#if DEBUG
if (CategoryName.StartsWith("Sentry.Samples.", StringComparison.Ordinal))
{
return false;
}
#endif

return CategoryName.StartsWith("Sentry.", StringComparison.Ordinal);
}
private bool IsFromSentry() => SentrySdkNamespaces.IsSentrySdk(CategoryName);

internal static bool IsEfExceptionMessage(EventId eventId)
{
Expand Down
1 change: 1 addition & 0 deletions src/Sentry.Serilog/Sentry.Serilog.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Using Include="Sentry" />
<Using Include="Sentry.Extensibility" />
<Using Include="Sentry.Infrastructure" />
<Using Include="Sentry.Internal" />
<Using Include="Sentry.Reflection" />
<Using Include="Sentry.Serilog" />
<Using Include="Serilog.Configuration" />
Expand Down
6 changes: 1 addition & 5 deletions src/Sentry.Serilog/SentrySink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private void InnerEmit(LogEvent logEvent)
{
if (logEvent.TryGetSourceContext(out var context))
{
if (IsSentryContext(context))
if (SentrySdkNamespaces.IsSentrySdk(context))
{
return;
}
Expand Down Expand Up @@ -175,10 +175,6 @@ private void InnerEmit(LogEvent logEvent)
}
}

private static bool IsSentryContext(string context) =>
context.StartsWith("Sentry.") ||
string.Equals(context, "Sentry", StringComparison.Ordinal);

private string FormatLogEvent(LogEvent logEvent)
{
if (_options.TextFormatter is { } formatter)
Expand Down
61 changes: 61 additions & 0 deletions src/Sentry/Internal/SentrySdkNamespaces.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
namespace Sentry.Internal;

/// <summary>
/// Identifies logger names that belong to the Sentry SDK itself, so the logging integrations can
/// skip them rather than feeding the SDK's own output back into Sentry.
/// </summary>
/// <remarks>
/// The <c>PackageRoots</c> half of this class is generated from <c>.craft.yml</c> - see the
/// <c>GenerateSentrySdkNamespaces</c> target in <c>Sentry.csproj</c>.
/// </remarks>
internal static partial class SentrySdkNamespaces
{
private const string Prefix = "Sentry";

/// <summary>
/// The core <c>Sentry</c> package can't be matched by prefix the way the other packages can:
/// all of its types live directly under <c>Sentry.</c>, and so does application code that
/// happens to use that namespace (<c>Sentry.Samples.*</c>, <c>Sentry.MyApp.*</c>). Matching on
/// <c>Sentry.</c> would therefore silently discard the user's own logs.
///
/// Core has no logging dependency of its own, so the only name it ever logs under is the one
/// <c>MelDiagnosticLogger</c> (in Sentry.Extensions.Logging) gets from
/// <c>ILogger&lt;ISentryClient&gt;</c>. Matching that exactly is enough.
/// </summary>
private static readonly string[] CoreNames = { Prefix, "Sentry.ISentryClient" };

/// <summary>
/// Whether <paramref name="loggerName"/> - a Serilog <c>SourceContext</c>, a
/// <c>Microsoft.Extensions.Logging</c> category, or an equivalent from another framework -
/// belongs to the Sentry SDK.
/// </summary>
internal static bool IsSentrySdk(string? loggerName)
{
if (loggerName is null || !loggerName.StartsWith(Prefix, StringComparison.Ordinal))
{
return false;
}

foreach (var name in CoreNames)
{
if (string.Equals(loggerName, name, StringComparison.Ordinal))
{
return true;
}
}

foreach (var root in PackageRoots)
{
// Either the package's own namespace, or something nested inside it. Comparing the
// character after the root keeps a package named e.g. 'Sentry.Maui' from matching an
// unrelated 'Sentry.MauiSomething'.
if (loggerName.StartsWith(root, StringComparison.Ordinal) &&
(loggerName.Length == root.Length || loggerName[root.Length] == '.'))
{
return true;
}
}

return false;
}
}
68 changes: 68 additions & 0 deletions src/Sentry/Sentry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,72 @@
</ItemGroup>
</Target>

<!--
The logging integrations need to recognise log messages that the SDK itself emitted, so they can
skip them instead of feeding them back into Sentry. That means knowing which namespace roots
belong to the SDK. Rather than hand-maintaining that list (and letting it go stale as packages
are added), it's generated from the package list in .craft.yml, which is already kept current for
releases. Consumed by Internal\SentrySdkNamespaces.cs.
-->
<PropertyGroup>
<_SentryCraftManifest>$(MSBuildThisFileDirectory)..\..\.craft.yml</_SentryCraftManifest>
</PropertyGroup>

<Target Name="GenerateSentrySdkNamespaces"
Inputs="$(_SentryCraftManifest);$(MSBuildThisFileFullPath)"
Outputs="$(IntermediateOutputPath)SentrySdkNamespaces.g.cs">

<ReadLinesFromFile File="$(_SentryCraftManifest)">
<Output TaskParameter="Lines" ItemName="_SentryCraftLine" />
</ReadLinesFromFile>

<ItemGroup>
<!-- Entries look like " nuget:Sentry.AspNetCore:" -->
<_SentrySdkPackage Include="$([System.Text.RegularExpressions.Regex]::Replace('%(_SentryCraftLine.Identity)', '^\s*nuget:([A-Za-z0-9\.]+):\s*$', '$1'))"
Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(_SentryCraftLine.Identity)', '^\s*nuget:[A-Za-z0-9\.]+:\s*$'))" />
<!--
'Sentry' is deliberately excluded. Every SDK type lives under the 'Sentry.' namespace, so
using it as a prefix would also match user code that happens to sit under 'Sentry.' - which
is the bug this list exists to avoid. The core package is matched explicitly instead, see
Internal\SentrySdkNamespaces.cs.
-->
<_SentrySdkPackage Remove="Sentry" />
</ItemGroup>

<Error Condition="'@(_SentrySdkPackage)' == ''"
Text="No 'nuget:&lt;package&gt;:' entries could be read from $(_SentryCraftManifest). Without them the logging integrations cannot identify the SDK's own log messages, which risks feedback loops - so this is a hard failure rather than an empty list." />

<ItemGroup>
<_SentrySdkNamespacesLine Include="// &lt;auto-generated/&gt;" />
<_SentrySdkNamespacesLine Include="// Generated from .craft.yml by the GenerateSentrySdkNamespaces target in Sentry.csproj." />
<_SentrySdkNamespacesLine Include="namespace Sentry.Internal%3B" />
<_SentrySdkNamespacesLine Include="internal static partial class SentrySdkNamespaces" />
<_SentrySdkNamespacesLine Include="{" />
<_SentrySdkNamespacesLine Include=" // Namespace roots owned by SDK packages other than the core 'Sentry' package." />
<_SentrySdkNamespacesLine Include=" internal static readonly string[] PackageRoots =" />
<_SentrySdkNamespacesLine Include=" {" />
<_SentrySdkNamespacesLine Include=" &quot;%(_SentrySdkPackage.Identity)&quot;," />
<_SentrySdkNamespacesLine Include=" }%3B" />
<_SentrySdkNamespacesLine Include="}" />
</ItemGroup>

<WriteLinesToFile File="$(IntermediateOutputPath)SentrySdkNamespaces.g.cs"
Lines="@(_SentrySdkNamespacesLine)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
</Target>

<!--
Kept separate from the target above so the Compile item is still added on incremental builds,
where that target is skipped as up to date.
-->
<Target Name="IncludeSentrySdkNamespaces"
BeforeTargets="BeforeCompile;CoreCompile"
DependsOnTargets="GenerateSentrySdkNamespaces">
<ItemGroup>
<Compile Include="$(IntermediateOutputPath)SentrySdkNamespaces.g.cs" />
<FileWrites Include="$(IntermediateOutputPath)SentrySdkNamespaces.g.cs" />
</ItemGroup>
</Target>

</Project>
34 changes: 15 additions & 19 deletions test/Sentry.Extensions.Logging.Tests/SentryLoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -218,24 +218,15 @@ public void LogError_DefaultOptions_CapturesEvent()
.CaptureEvent(Arg.Any<SentryEvent>());
}

[Fact]
public void Log_SentryCategory_DoesNotSendEvent()
[Theory]
[InlineData("Sentry")]
[InlineData("Sentry.ISentryClient")]
[InlineData("Sentry.Extensions.Logging.SentryLogger")]
[InlineData("Sentry.AspNetCore.SentryMiddleware")]
public void Log_SentryCategory_DoesNotSendEvent(string categoryName)
{
var expectedException = new Exception("expected message");
_fixture.CategoryName = "Sentry.Some.Class";
var sut = _fixture.GetSut();

sut.Log<object>(LogLevel.Critical, default, null, expectedException, null);

_ = _fixture.Hub.DidNotReceive()
.CaptureEvent(Arg.Any<SentryEvent>());
}

[Fact]
public void Log_SentryRootCategory_DoesNotSendEvent()
{
var expectedException = new Exception("expected message");
_fixture.CategoryName = "Sentry";
_fixture.CategoryName = categoryName;
var sut = _fixture.GetSut();

sut.Log<object>(LogLevel.Critical, default, null, expectedException, null);
Expand All @@ -244,12 +235,17 @@ public void Log_SentryRootCategory_DoesNotSendEvent()
.CaptureEvent(Arg.Any<SentryEvent>());
}

// A category under a "Sentry" namespace that isn't one of ours belongs to the application.
// https://github.com/getsentry/sentry-dotnet/issues/132
[Fact]
public void Log_SentrySomethingCategory_SendEvent()
// https://github.com/getsentry/sentry-dotnet/issues/5265
[Theory]
[InlineData("SentrySomething")]
[InlineData("Sentry.Some.Class")]
[InlineData("Sentry.Samples.AspNetCore.Serilog.Program")]
public void Log_UserCategory_SendsEvent(string categoryName)
{
var expectedException = new Exception("expected message");
_fixture.CategoryName = "SentrySomething";
_fixture.CategoryName = categoryName;
var sut = _fixture.GetSut();

sut.Log<object>(LogLevel.Critical, default, null, expectedException, null);
Expand Down
41 changes: 41 additions & 0 deletions test/Sentry.Serilog.Tests/AspNetCoreIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,46 @@ public async Task StructuredLogging_Enabled()
Assert.NotEmpty(Logs);
Assert.Contains(Logs, log => log.Level == SentryLogLevel.Info && log.Message == "Hello, World!");
}

[Fact]
public async Task ILoggerFromApplicationNamespaceStartingWithSentry_CapturesEventAndRunsEventProcessors()
{
// Logs from our samples shouldn't be filtered - only logs from our SDK should be
const string category = "Sentry.Samples.AspNetCore.Serilog.Program";

var processor = new RecordingEventProcessor();
ConfigureServices = services => services.AddSingleton<ISentryEventProcessor>(processor);

var handler = new RequestHandler
{
Path = "/log",
Handler = context =>
{
context.RequestServices.GetRequiredService<ILoggerFactory>()
.CreateLogger(category)
.LogError(new InvalidOperationException("hello?"), "This is a problem");
return Task.CompletedTask;
}
};

Handlers = new[] { handler };
Build();
await HttpClient.GetAsync(handler.Path);
await ServiceProvider.GetRequiredService<IHub>().FlushAsync();

Assert.Contains(Events, e => e.Logger == category);
Assert.True(processor.Invoked);
}

private class RecordingEventProcessor : ISentryEventProcessor
{
public bool Invoked { get; private set; }

public SentryEvent Process(SentryEvent @event)
{
Invoked = true;
return @event;
}
}
}
#endif
48 changes: 24 additions & 24 deletions test/Sentry.Serilog.Tests/SentrySinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,60 +281,60 @@ public void Emit_WithTextFormatter_EventCaptured()
&& p.Message.Message == expectedMessage));
}

[Fact]
public void Emit_SourceContextMatchesSentry_NoEventSent()
{
var sut = _fixture.GetSut();

var evt = new LogEvent(DateTimeOffset.UtcNow, LogEventLevel.Error, null,
new MessageTemplateParser().Parse("message"),
new[] { new LogEventProperty("SourceContext", new ScalarValue("Sentry.Serilog")) });

sut.Emit(evt);

_fixture.Hub.DidNotReceive().CaptureEvent(Arg.Any<SentryEvent>());
}

[Fact]
public void Emit_SourceContextContainsSentry_NoEventSent()
[Theory]
[InlineData("Sentry")]
[InlineData("Sentry.ISentryClient")]
[InlineData("Sentry.Serilog")]
[InlineData("Sentry.Serilog.SentrySink")]
[InlineData("Sentry.AspNetCore.SentryMiddleware")]
[InlineData("Sentry.Extensions.Logging.MelDiagnosticLogger")]
public void Emit_SourceContextFromSentrySdk_NoEventSent(string sourceContext)
{
var sut = _fixture.GetSut();

var evt = new LogEvent(DateTimeOffset.UtcNow, LogEventLevel.Error, null,
new MessageTemplateParser().Parse("message"),
new[] { new LogEventProperty("SourceContext", new ScalarValue("Sentry")) });
new[] { new LogEventProperty("SourceContext", new ScalarValue(sourceContext)) });

sut.Emit(evt);

_fixture.Hub.DidNotReceive().CaptureEvent(Arg.Any<SentryEvent>());
}

[Fact]
public void Emit_SourceContextMatchesSentry_NoScopeConfigured()
[Theory]
[InlineData("Sentry")]
[InlineData("Sentry.ISentryClient")]
[InlineData("Sentry.Serilog")]
[InlineData("Sentry.AspNetCore.SentryMiddleware")]
public void Emit_SourceContextFromSentrySdk_NoScopeConfigured(string sourceContext)
{
var sut = _fixture.GetSut();

var evt = new LogEvent(DateTimeOffset.UtcNow, LogEventLevel.Error, null,
new MessageTemplateParser().Parse("message"),
new[] { new LogEventProperty("SourceContext", new ScalarValue("Sentry.Serilog")) });
new[] { new LogEventProperty("SourceContext", new ScalarValue(sourceContext)) });

sut.Emit(evt);

_fixture.Hub.DidNotReceive().ConfigureScope(Arg.Any<Action<Scope>>());
}

[Fact]
public void Emit_SourceContextContainsSentry_NoScopeConfigured()
// See https://github.com/getsentry/sentry-dotnet/issues/5265
[Theory]
[InlineData("Sentry.Samples.AspNetCore.Serilog.Program")]
[InlineData("Sentry.Some.Class")]
[InlineData("SentrySomething")]
public void Emit_SourceContextFromUserCode_EventSent(string sourceContext)
{
var sut = _fixture.GetSut();

var evt = new LogEvent(DateTimeOffset.UtcNow, LogEventLevel.Error, null,
new MessageTemplateParser().Parse("message"),
new[] { new LogEventProperty("SourceContext", new ScalarValue("Sentry")) });
new[] { new LogEventProperty("SourceContext", new ScalarValue(sourceContext)) });

sut.Emit(evt);

_fixture.Hub.DidNotReceive().ConfigureScope(Arg.Any<Action<Scope>>());
_fixture.Hub.Received(1).CaptureEvent(Arg.Is<SentryEvent>(e => e.Logger == sourceContext));
}

[Fact]
Expand Down
Loading
Loading