Skip to content

Commit f2e1ba2

Browse files
committed
test(logs): add tests
1 parent 58dce74 commit f2e1ba2

File tree

5 files changed

+318
-9
lines changed

5 files changed

+318
-9
lines changed

src/Sentry/Internal/Hub.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,15 @@ internal Hub(
5757
client ??= new SentryClient(options, randomValuesFactory: _randomValuesFactory, sessionManager: _sessionManager);
5858

5959
ScopeManager = scopeManager ?? new SentryScopeManager(options, client);
60-
Logger = new SentryStructuredLogger(this);
6160

6261
if (!options.IsGlobalModeEnabled)
6362
{
6463
// Push the first scope so the async local starts from here
6564
PushScope();
6665
}
6766

67+
Logger = new SentryStructuredLogger(this);
68+
6869
#if MEMORY_DUMP_SUPPORTED
6970
if (options.HeapDumpOptions is not null)
7071
{

src/Sentry/Protocol/SentryLog.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,12 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
258258
SentryAttributeSerializer.WriteAttribute(writer, "sentry.message.template", Template, "string");
259259
}
260260

261-
for (var index = 0; index < Parameters.Length; index++)
261+
if (!Parameters.IsDefault)
262262
{
263-
SentryAttributeSerializer.WriteAttribute(writer, $"sentry.message.parameters.{index}", Parameters[index]);
263+
for (var index = 0; index < Parameters.Length; index++)
264+
{
265+
SentryAttributeSerializer.WriteAttribute(writer, $"sentry.message.parameters.{index}", Parameters[index]);
266+
}
264267
}
265268

266269
foreach (var attribute in _attributes)

src/Sentry/SentryStructuredLogger.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ public sealed class SentryStructuredLogger
2020
private readonly IInternalScopeManager? _scopeManager;
2121

2222
internal SentryStructuredLogger(IHub hub)
23-
: this(hub, SystemClock.Clock)
24-
{
25-
}
26-
27-
internal SentryStructuredLogger(IHub hub, ISystemClock clock)
28-
: this(hub, (hub as Hub)?.ScopeManager, hub.GetSentryOptions(), clock)
23+
: this(hub, (hub as Hub)?.ScopeManager, hub.GetSentryOptions(), SystemClock.Clock)
2924
{
3025
}
3126

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
namespace Sentry.Tests.Protocol;
2+
3+
/// <summary>
4+
/// <see href="https://develop.sentry.dev/sdk/telemetry/logs/"/>
5+
/// </summary>
6+
public class SentryLogLevelTests
7+
{
8+
[Theory]
9+
[MemberData(nameof(SeverityTextAndSeverityNumber))]
10+
public void SeverityTextAndSeverityNumber_WithinRange_MatchesProtocol(int level, string text, int? number)
11+
{
12+
var @enum = (SentryLogLevel)level;
13+
14+
var (severityText, severityNumber) = @enum.ToSeverityTextAndOptionalSeverityNumber();
15+
16+
Assert.Multiple(
17+
() => Assert.Equal(text, severityText),
18+
() => Assert.Equal(number, severityNumber));
19+
}
20+
21+
[Theory]
22+
[InlineData(0)]
23+
[InlineData(25)]
24+
public void SeverityTextAndSeverityNumber_OutOfRange_ThrowOutOfRange(int level)
25+
{
26+
var @enum = (SentryLogLevel)level;
27+
28+
var exception = Assert.Throws<ArgumentOutOfRangeException>("level", () => @enum.ToSeverityTextAndOptionalSeverityNumber());
29+
Assert.StartsWith("Severity must be between 1 (inclusive) and 24 (inclusive).", exception.Message);
30+
Assert.Equal(level, (int)exception.ActualValue!);
31+
}
32+
33+
[Fact]
34+
public void ThrowOutOfRange_WithinRange_DoesNotThrow()
35+
{
36+
var range = Enumerable.Range(1, 24);
37+
38+
var count = 0;
39+
foreach (var item in range)
40+
{
41+
var level = (SentryLogLevel)item;
42+
SentryLogLevelExtensions.ThrowIfOutOfRange(level);
43+
count++;
44+
}
45+
46+
Assert.Equal(24, count);
47+
}
48+
49+
[Theory]
50+
[InlineData(0)]
51+
[InlineData(25)]
52+
public void ThrowOutOfRange_OutOfRange_Throws(int level)
53+
{
54+
var @enum = (SentryLogLevel)level;
55+
56+
var exception = Assert.Throws<ArgumentOutOfRangeException>("@enum", () => SentryLogLevelExtensions.ThrowIfOutOfRange(@enum));
57+
Assert.StartsWith("Severity must be between 1 (inclusive) and 24 (inclusive).", exception.Message);
58+
Assert.Equal(level, (int)exception.ActualValue!);
59+
}
60+
61+
public static TheoryData<int, string, int?> SeverityTextAndSeverityNumber()
62+
{
63+
return new TheoryData<int, string, int?>
64+
{
65+
{ 1, "trace", null },
66+
{ 2, "trace", 2 },
67+
{ 3, "trace", 3 },
68+
{ 4, "trace", 4 },
69+
{ 5, "debug", null },
70+
{ 6, "debug", 6 },
71+
{ 7, "debug", 7 },
72+
{ 8, "debug", 8 },
73+
{ 9, "info", null },
74+
{ 10, "info", 10 },
75+
{ 11, "info", 11 },
76+
{ 12, "info", 12 },
77+
{ 13, "warn", null },
78+
{ 14, "warn", 14 },
79+
{ 15, "warn", 15 },
80+
{ 16, "warn", 16 },
81+
{ 17, "error", null },
82+
{ 18, "error", 18 },
83+
{ 19, "error", 19 },
84+
{ 20, "error", 20 },
85+
{ 21, "fatal", null },
86+
{ 22, "fatal", 22 },
87+
{ 23, "fatal", 23 },
88+
{ 24, "fatal", 24 },
89+
};
90+
}
91+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#nullable enable
2+
3+
namespace Sentry.Tests;
4+
5+
/// <summary>
6+
/// <see href="https://develop.sentry.dev/sdk/telemetry/logs/"/>
7+
/// </summary>
8+
public class SentryStructuredLoggerTests
9+
{
10+
internal sealed class Fixture
11+
{
12+
public Fixture()
13+
{
14+
Hub = Substitute.For<IHub>();
15+
ScopeManager = Substitute.For<IInternalScopeManager>();
16+
Options = new SentryOptions();
17+
Clock = new MockClock(new DateTimeOffset(2025, 04, 22, 14, 51, 00, TimeSpan.Zero));
18+
Span = Substitute.For<ISpan>();
19+
TraceId = SentryId.Create();
20+
ParentSpanId = SpanId.Create();
21+
22+
Hub.GetSpan().Returns(Span);
23+
Span.TraceId.Returns(TraceId);
24+
Span.ParentSpanId.Returns(ParentSpanId);
25+
}
26+
27+
public IHub Hub { get; }
28+
public IInternalScopeManager ScopeManager { get; }
29+
public SentryOptions Options { get; }
30+
public ISystemClock Clock { get; }
31+
public ISpan Span { get; }
32+
public SentryId TraceId { get; }
33+
public SpanId? ParentSpanId { get; }
34+
35+
public void UseScopeManager()
36+
{
37+
Hub.GetSpan().Returns((ISpan?)null);
38+
39+
var propagationContext = new SentryPropagationContext(TraceId, ParentSpanId!.Value);
40+
var scope = new Scope(Options, propagationContext);
41+
var scopeAndClient = new KeyValuePair<Scope, ISentryClient>(scope, null!);
42+
ScopeManager.GetCurrent().Returns(scopeAndClient);
43+
}
44+
45+
public SentryStructuredLogger GetSut() => new(Hub, ScopeManager, Options, Clock);
46+
}
47+
48+
private readonly Fixture _fixture;
49+
50+
public SentryStructuredLoggerTests()
51+
{
52+
_fixture = new Fixture();
53+
}
54+
55+
[Theory]
56+
[InlineData(SentryLogLevel.Trace)]
57+
[InlineData(SentryLogLevel.Debug)]
58+
[InlineData(SentryLogLevel.Info)]
59+
[InlineData(SentryLogLevel.Warning)]
60+
[InlineData(SentryLogLevel.Error)]
61+
[InlineData(SentryLogLevel.Fatal)]
62+
public void Log_Enabled_CapturesEnvelope(SentryLogLevel level)
63+
{
64+
_fixture.Options.EnableLogs = true;
65+
var logger = _fixture.GetSut();
66+
67+
Envelope envelope = null!;
68+
_fixture.Hub.CaptureEnvelope(Arg.Do<Envelope>(arg => envelope = arg));
69+
70+
logger.Log(level, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog);
71+
72+
_fixture.Hub.Received(1).CaptureEnvelope(Arg.Any<Envelope>());
73+
envelope.AssertEnvelope(_fixture, level);
74+
}
75+
76+
[Theory]
77+
[InlineData(SentryLogLevel.Trace)]
78+
[InlineData(SentryLogLevel.Debug)]
79+
[InlineData(SentryLogLevel.Info)]
80+
[InlineData(SentryLogLevel.Warning)]
81+
[InlineData(SentryLogLevel.Error)]
82+
[InlineData(SentryLogLevel.Fatal)]
83+
public void Log_Disabled_DoesNotCaptureEnvelope(SentryLogLevel level)
84+
{
85+
_fixture.Options.EnableLogs.Should().BeFalse();
86+
var logger = _fixture.GetSut();
87+
88+
logger.Log(level, "Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog);
89+
90+
_fixture.Hub.Received(0).CaptureEnvelope(Arg.Any<Envelope>());
91+
}
92+
93+
[Fact]
94+
public void Log_UseScopeManager_CapturesEnvelope()
95+
{
96+
_fixture.UseScopeManager();
97+
_fixture.Options.EnableLogs = true;
98+
var logger = _fixture.GetSut();
99+
100+
Envelope envelope = null!;
101+
_fixture.Hub.CaptureEnvelope(Arg.Do<Envelope>(arg => envelope = arg));
102+
103+
logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog);
104+
105+
_fixture.Hub.Received(1).CaptureEnvelope(Arg.Any<Envelope>());
106+
envelope.AssertEnvelope(_fixture, SentryLogLevel.Trace);
107+
}
108+
109+
[Fact]
110+
public void Log_WithBeforeSendLog_InvokesCallback()
111+
{
112+
var invocations = 0;
113+
SentryLog configuredLog = null!;
114+
115+
_fixture.Options.EnableLogs = true;
116+
_fixture.Options.SetBeforeSendLog((SentryLog log) =>
117+
{
118+
invocations++;
119+
configuredLog = log;
120+
return log;
121+
});
122+
var logger = _fixture.GetSut();
123+
124+
logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog);
125+
126+
_fixture.Hub.Received(1).CaptureEnvelope(Arg.Any<Envelope>());
127+
invocations.Should().Be(1);
128+
configuredLog.AssertLog(_fixture, SentryLogLevel.Trace);
129+
}
130+
131+
[Fact]
132+
public void Log_WhenBeforeSendLogReturnsNull_DoesNotCaptureEnvelope()
133+
{
134+
var invocations = 0;
135+
136+
_fixture.Options.EnableLogs = true;
137+
_fixture.Options.SetBeforeSendLog((SentryLog log) =>
138+
{
139+
invocations++;
140+
return null;
141+
});
142+
var logger = _fixture.GetSut();
143+
144+
logger.LogTrace("Template string with arguments: {0}, {1}, {2}, {3}", ["string", true, 1, 2.2], ConfigureLog);
145+
146+
_fixture.Hub.Received(0).CaptureEnvelope(Arg.Any<Envelope>());
147+
invocations.Should().Be(1);
148+
}
149+
150+
private static void ConfigureLog(SentryLog log)
151+
{
152+
log.SetAttribute("attribute-key", "attribute-value");
153+
}
154+
}
155+
156+
file static class AssertionExtensions
157+
{
158+
public static void AssertEnvelope(this Envelope envelope, SentryStructuredLoggerTests.Fixture fixture, SentryLogLevel level)
159+
{
160+
envelope.Header.Should().ContainSingle().Which.Key.Should().Be("sdk");
161+
var item = envelope.Items.Should().ContainSingle().Which;
162+
163+
var log = item.Payload.Should().BeOfType<JsonSerializable>().Which.Source.Should().BeOfType<SentryLog>().Which;
164+
AssertLog(log, fixture, level);
165+
166+
Assert.Collection(item.Header,
167+
element => Assert.Equal(CreateHeader("type", "log"), element),
168+
element => Assert.Equal(CreateHeader("item_count", 1), element),
169+
element => Assert.Equal(CreateHeader("content_type", "application/vnd.sentry.items.log+json"), element));
170+
}
171+
172+
public static void AssertLog(this SentryLog log, SentryStructuredLoggerTests.Fixture fixture, SentryLogLevel level)
173+
{
174+
log.Timestamp.Should().Be(fixture.Clock.GetUtcNow());
175+
log.TraceId.Should().Be(fixture.TraceId);
176+
log.Level.Should().Be(level);
177+
log.Message.Should().Be("Template string with arguments: string, True, 1, 2.2");
178+
log.Template.Should().Be("Template string with arguments: {0}, {1}, {2}, {3}");
179+
log.Parameters.Should().BeEquivalentTo(new object[] { "string", true, 1, 2.2 } );
180+
log.ParentSpanId.Should().Be(fixture.ParentSpanId);
181+
log.TryGetAttribute("attribute-key", out string? value).Should().BeTrue();
182+
value.Should().Be("attribute-value");
183+
}
184+
185+
private static KeyValuePair<string, object?> CreateHeader(string name, object? value)
186+
{
187+
return new KeyValuePair<string, object?>(name, value);
188+
}
189+
}
190+
191+
file static class SentryStructuredLoggerExtensions
192+
{
193+
public static void Log(this SentryStructuredLogger logger, SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog)
194+
{
195+
switch (level)
196+
{
197+
case SentryLogLevel.Trace:
198+
logger.LogTrace(template, parameters, configureLog);
199+
break;
200+
case SentryLogLevel.Debug:
201+
logger.LogDebug(template, parameters, configureLog);
202+
break;
203+
case SentryLogLevel.Info:
204+
logger.LogInfo(template, parameters, configureLog);
205+
break;
206+
case SentryLogLevel.Warning:
207+
logger.LogWarning(template, parameters, configureLog);
208+
break;
209+
case SentryLogLevel.Error:
210+
logger.LogError(template, parameters, configureLog);
211+
break;
212+
case SentryLogLevel.Fatal:
213+
logger.LogFatal(template, parameters, configureLog);
214+
break;
215+
default:
216+
throw new ArgumentOutOfRangeException(nameof(level), level, null);
217+
}
218+
}
219+
}

0 commit comments

Comments
 (0)