From 8ec35ea362f6ff40e896859e8ca3297e7b0548f2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 20 Feb 2026 11:51:06 +1300 Subject: [PATCH 01/10] Refactored SentryAttributes into a separate class --- src/Sentry/Protocol/SentryAttributes.cs | 176 ++++++++++++++++++ src/Sentry/SentryMetric.Factory.cs | 6 +- src/Sentry/SentryMetric.cs | 109 +---------- .../SentryAttributesExtensions.cs | 16 ++ test/Sentry.Tests/SentryMetricTests.cs | 26 +-- 5 files changed, 211 insertions(+), 122 deletions(-) create mode 100644 src/Sentry/Protocol/SentryAttributes.cs create mode 100644 test/Sentry.Testing/SentryAttributesExtensions.cs diff --git a/src/Sentry/Protocol/SentryAttributes.cs b/src/Sentry/Protocol/SentryAttributes.cs new file mode 100644 index 0000000000..8c33e02abc --- /dev/null +++ b/src/Sentry/Protocol/SentryAttributes.cs @@ -0,0 +1,176 @@ +using Sentry.Extensibility; + +namespace Sentry.Protocol; + +internal class SentryAttributes : Dictionary, ISentryJsonSerializable +{ + public SentryAttributes() : base(StringComparer.Ordinal) + { + } + + public SentryAttributes(int capacity) : base(capacity, StringComparer.Ordinal) + { + } + + /// + /// Gets the attribute value associated with the specified key. + /// + /// + /// Returns if this contains an attribute with the specified key which is of type and it's value is not . + /// Otherwise . + /// Supported types: + /// + /// + /// Type + /// Range + /// + /// + /// string + /// and + /// + /// + /// boolean + /// and + /// + /// + /// integer + /// 64-bit signed integral numeric types + /// + /// + /// double + /// 64-bit floating-point numeric types + /// + /// + /// Unsupported types: + /// + /// + /// Type + /// Result + /// + /// + /// + /// ToString as "type": "string" + /// + /// + /// Collections + /// ToString as "type": "string" + /// + /// + /// + /// ignored + /// + /// + /// + /// + public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out TAttribute value) + { + if (TryGetValue(key, out var attribute) && attribute.Value is TAttribute attributeValue) + { + value = attributeValue; + return true; + } + + value = default; + return false; + } + + /// + /// Set a key-value pair of data attached to the metric. + /// + public void SetAttribute(string key, TAttribute value) where TAttribute : notnull + { + if (value is null) + { + return; + } + + this[key] = new SentryAttribute(value); + } + + internal void SetAttribute(string key, string value) + { + this[key] = new SentryAttribute(value, "string"); + } + + internal void SetAttribute(string key, char value) + { + this[key] = new SentryAttribute(value.ToString(), "string"); + } + + internal void SetAttribute(string key, int value) + { + this[key] = new SentryAttribute(value, "integer"); + } + + internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) + { + var environment = options.SettingLocator.GetEnvironment(); + SetAttribute("sentry.environment", environment); + + var release = options.SettingLocator.GetRelease(); + if (release is not null) + { + SetAttribute("sentry.release", release); + } + + if (sdk.Name is { } name) + { + SetAttribute("sentry.sdk.name", name); + } + if (sdk.Version is { } version) + { + SetAttribute("sentry.sdk.version", version); + } + } + + internal void SetAttributes(IEnumerable>? attributes) + { + if (attributes is null) + { + return; + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + if (attributes.TryGetNonEnumeratedCount(out var count)) + { + _ = EnsureCapacity(Count + count); + } +#endif + + foreach (var attribute in attributes) + { + this[attribute.Key] = new SentryAttribute(attribute.Value); + } + } + + internal void SetAttributes(ReadOnlySpan> attributes) + { + if (attributes.IsEmpty) + { + return; + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + _ = EnsureCapacity(Count + attributes.Length); +#endif + + foreach (var attribute in attributes) + { + this[attribute.Key] = new SentryAttribute(attribute.Value); + } + } + + /// + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WritePropertyName("attributes"); + writer.WriteStartObject(); + + foreach (var attribute in this) + { + SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Sentry/SentryMetric.Factory.cs b/src/Sentry/SentryMetric.Factory.cs index fe6d0e7b2d..7c9621ce84 100644 --- a/src/Sentry/SentryMetric.Factory.cs +++ b/src/Sentry/SentryMetric.Factory.cs @@ -20,7 +20,7 @@ private static SentryMetric CreateCore(IHub hub, SentryOptions options, IS }; scope ??= hub.GetScope(); - metric.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); + metric.Attributes.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance); return metric; } @@ -28,14 +28,14 @@ private static SentryMetric CreateCore(IHub hub, SentryOptions options, IS internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, IEnumerable>? attributes, Scope? scope) where T : struct { var metric = CreateCore(hub, options, clock, type, name, value, unit, scope); - metric.SetAttributes(attributes); + metric.Attributes.SetAttributes(attributes); return metric; } internal static SentryMetric Create(IHub hub, SentryOptions options, ISystemClock clock, SentryMetricType type, string name, T value, string? unit, ReadOnlySpan> attributes, Scope? scope) where T : struct { var metric = CreateCore(hub, options, clock, type, name, value, unit, scope); - metric.SetAttributes(attributes); + metric.Attributes.SetAttributes(attributes); return metric; } diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs index ae56f0e019..c74dfaf360 100644 --- a/src/Sentry/SentryMetric.cs +++ b/src/Sentry/SentryMetric.cs @@ -14,8 +14,6 @@ namespace Sentry; [DebuggerDisplay(@"SentryMetric \{ Type = {Type}, Name = '{Name}', Value = {Value} \}")] public abstract partial class SentryMetric { - private readonly Dictionary _attributes; - [SetsRequiredMembers] private protected SentryMetric(DateTimeOffset timestamp, SentryId traceId, SentryMetricType type, string name) { @@ -24,7 +22,7 @@ private protected SentryMetric(DateTimeOffset timestamp, SentryId traceId, Sentr Type = type; Name = name; // 7 is the number of built-in attributes, so we start with that. - _attributes = new Dictionary(7); + Attributes = new SentryAttributes(7); } /// @@ -114,6 +112,8 @@ private protected SentryMetric(DateTimeOffset timestamp, SentryId traceId, Sentr /// public string? Unit { get; init; } + internal SentryAttributes Attributes { get; } + /// /// Gets the metric value if it is of the specified type . /// @@ -174,102 +174,13 @@ private protected SentryMetric(DateTimeOffset timestamp, SentryId traceId, Sentr /// /// public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out TAttribute value) - { - if (_attributes.TryGetValue(key, out var attribute) && attribute.Value is TAttribute attributeValue) - { - value = attributeValue; - return true; - } - - value = default; - return false; - } + => Attributes.TryGetAttribute(key, out value); /// /// Set a key-value pair of data attached to the metric. /// public void SetAttribute(string key, TAttribute value) where TAttribute : notnull - { - if (value is null) - { - return; - } - - _attributes[key] = new SentryAttribute(value); - } - - internal void SetAttribute(string key, string value) - { - _attributes[key] = new SentryAttribute(value, "string"); - } - - internal void SetAttribute(string key, char value) - { - _attributes[key] = new SentryAttribute(value.ToString(), "string"); - } - - internal void SetAttribute(string key, int value) - { - _attributes[key] = new SentryAttribute(value, "integer"); - } - - internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) - { - var environment = options.SettingLocator.GetEnvironment(); - SetAttribute("sentry.environment", environment); - - var release = options.SettingLocator.GetRelease(); - if (release is not null) - { - SetAttribute("sentry.release", release); - } - - if (sdk.Name is { } name) - { - SetAttribute("sentry.sdk.name", name); - } - if (sdk.Version is { } version) - { - SetAttribute("sentry.sdk.version", version); - } - } - - internal void SetAttributes(IEnumerable>? attributes) - { - if (attributes is null) - { - return; - } - -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - if (attributes.TryGetNonEnumeratedCount(out var count)) - { - _ = _attributes.EnsureCapacity(_attributes.Count + count); - } -#endif - - foreach (var attribute in attributes) - { - _attributes[attribute.Key] = new SentryAttribute(attribute.Value); - } - } - - internal void SetAttributes(ReadOnlySpan> attributes) - { - if (attributes.IsEmpty) - { - return; - } - -#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER - _ = _attributes.EnsureCapacity(_attributes.Count + attributes.Length); -#endif - - foreach (var attribute in attributes) - { - _attributes[attribute.Key] = new SentryAttribute(attribute.Value); - } - } + => Attributes.SetAttribute(key, value); /// internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) @@ -300,15 +211,7 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteString("unit", Unit); } - writer.WritePropertyName("attributes"); - writer.WriteStartObject(); - - foreach (var attribute in _attributes) - { - SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger); - } - - writer.WriteEndObject(); + Attributes.WriteTo(writer, logger); writer.WriteEndObject(); } diff --git a/test/Sentry.Testing/SentryAttributesExtensions.cs b/test/Sentry.Testing/SentryAttributesExtensions.cs new file mode 100644 index 0000000000..2d39c0f011 --- /dev/null +++ b/test/Sentry.Testing/SentryAttributesExtensions.cs @@ -0,0 +1,16 @@ +namespace Sentry.Testing; + +public static class SentryAttributesExtensions +{ + internal static void ShouldContain(this SentryAttributes attributes, string key, T expected) + { + attributes.TryGetAttribute(key, out var value).Should().BeTrue(); + value.Should().Be(expected); + } + + internal static void ShouldNotContain(this SentryAttributes attributes, string key, T expected) + { + attributes.TryGetAttribute(key, out var value).Should().BeFalse(); + value.Should().Be(default(T)); + } +} diff --git a/test/Sentry.Tests/SentryMetricTests.cs b/test/Sentry.Tests/SentryMetricTests.cs index 2614c99989..a7b1406277 100644 --- a/test/Sentry.Tests/SentryMetricTests.cs +++ b/test/Sentry.Tests/SentryMetricTests.cs @@ -42,7 +42,7 @@ public void Protocol_Default_VerifyAttributes() Unit = "test_unit", }; metric.SetAttribute("attribute", "value"); - metric.SetDefaultAttributes(options, sdk); + metric.Attributes.SetDefaultAttributes(options, sdk); metric.Timestamp.Should().Be(Timestamp); metric.TraceId.Should().Be(TraceId); @@ -52,18 +52,12 @@ public void Protocol_Default_VerifyAttributes() metric.SpanId.Should().Be(SpanId); metric.Unit.Should().BeEquivalentTo("test_unit"); - metric.TryGetAttribute("attribute", out var attribute).Should().BeTrue(); - attribute.Should().Be("value"); - metric.TryGetAttribute("sentry.environment", out var environment).Should().BeTrue(); - environment.Should().Be(options.Environment); - metric.TryGetAttribute("sentry.release", out var release).Should().BeTrue(); - release.Should().Be(options.Release); - metric.TryGetAttribute("sentry.sdk.name", out var name).Should().BeTrue(); - name.Should().Be(sdk.Name); - metric.TryGetAttribute("sentry.sdk.version", out var version).Should().BeTrue(); - version.Should().Be(sdk.Version); - metric.TryGetAttribute("not-found", out var notFound).Should().BeFalse(); - notFound.Should().BeNull(); + metric.Attributes.ShouldContain("attribute", "value"); + metric.Attributes.ShouldContain("sentry.environment", options.Environment); + metric.Attributes.ShouldContain("sentry.release", options.Release); + metric.Attributes.ShouldContain("sentry.sdk.name", sdk.Name); + metric.Attributes.ShouldContain("sentry.sdk.version", sdk.Version); + metric.Attributes.ShouldNotContain("not-found", "value"); } [Fact] @@ -76,7 +70,7 @@ public void WriteTo_Envelope_MinimalSerializedSentryMetric() }; var metric = new SentryMetric(Timestamp, TraceId, SentryMetricType.Counter, "sentry_tests.sentry_metric_tests.counter", 1); - metric.SetDefaultAttributes(options, new SdkVersion()); + metric.Attributes.SetDefaultAttributes(options, new SdkVersion()); var envelope = Envelope.FromMetric(new TraceMetric([metric])); @@ -154,7 +148,7 @@ public void WriteTo_EnvelopeItem_MaximalSerializedSentryMetric() metric.SetAttribute("boolean-attribute", true); metric.SetAttribute("integer-attribute", 3); metric.SetAttribute("double-attribute", 4.4); - metric.SetDefaultAttributes(options, new SdkVersion { Name = "Sentry.Test.SDK", Version = "1.2.3-test+Sentry" }); + metric.Attributes.SetDefaultAttributes(options, new SdkVersion { Name = "Sentry.Test.SDK", Version = "1.2.3-test+Sentry" }); var envelope = EnvelopeItem.FromMetric(new TraceMetric([metric])); @@ -360,7 +354,7 @@ public void WriteTo_Attributes_AsJson() #else metric.SetAttribute("object", new KeyValuePair("key", "value")); #endif - metric.SetAttribute("null", null!); + metric.Attributes.SetAttribute("null", null!); var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var attributes = document.RootElement.GetProperty("attributes"); From 012ba110fa7371f78b97fcaf1bd6b0ab94172fc9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 23 Feb 2026 10:25:23 +1300 Subject: [PATCH 02/10] Review feedback --- test/Sentry.Testing/SentryAttributesExtensions.cs | 2 +- test/Sentry.Tests/SentryMetricTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Sentry.Testing/SentryAttributesExtensions.cs b/test/Sentry.Testing/SentryAttributesExtensions.cs index 2d39c0f011..d022138c2c 100644 --- a/test/Sentry.Testing/SentryAttributesExtensions.cs +++ b/test/Sentry.Testing/SentryAttributesExtensions.cs @@ -8,7 +8,7 @@ internal static void ShouldContain(this SentryAttributes attributes, string k value.Should().Be(expected); } - internal static void ShouldNotContain(this SentryAttributes attributes, string key, T expected) + internal static void ShouldNotContain(this SentryAttributes attributes, string key) { attributes.TryGetAttribute(key, out var value).Should().BeFalse(); value.Should().Be(default(T)); diff --git a/test/Sentry.Tests/SentryMetricTests.cs b/test/Sentry.Tests/SentryMetricTests.cs index a7b1406277..5ca62feea9 100644 --- a/test/Sentry.Tests/SentryMetricTests.cs +++ b/test/Sentry.Tests/SentryMetricTests.cs @@ -57,7 +57,7 @@ public void Protocol_Default_VerifyAttributes() metric.Attributes.ShouldContain("sentry.release", options.Release); metric.Attributes.ShouldContain("sentry.sdk.name", sdk.Name); metric.Attributes.ShouldContain("sentry.sdk.version", sdk.Version); - metric.Attributes.ShouldNotContain("not-found", "value"); + metric.Attributes.ShouldNotContain("not-found"); } [Fact] From e67baee1c88b63e3ee032ccf481f38c39577678b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Mon, 23 Feb 2026 10:58:33 +1300 Subject: [PATCH 03/10] Review feedback --- src/Sentry/Protocol/SentryAttributes.cs | 1 - src/Sentry/SentryMetric.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Protocol/SentryAttributes.cs b/src/Sentry/Protocol/SentryAttributes.cs index 8c33e02abc..211a3591c5 100644 --- a/src/Sentry/Protocol/SentryAttributes.cs +++ b/src/Sentry/Protocol/SentryAttributes.cs @@ -163,7 +163,6 @@ internal void SetAttributes(ReadOnlySpan> attribute /// public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { - writer.WritePropertyName("attributes"); writer.WriteStartObject(); foreach (var attribute in this) diff --git a/src/Sentry/SentryMetric.cs b/src/Sentry/SentryMetric.cs index c74dfaf360..a9c0214944 100644 --- a/src/Sentry/SentryMetric.cs +++ b/src/Sentry/SentryMetric.cs @@ -211,6 +211,7 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteString("unit", Unit); } + writer.WritePropertyName("attributes"); Attributes.WriteTo(writer, logger); writer.WriteEndObject(); From cea167afa1b404e51520744e77b50a48d912f562 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 5 Mar 2026 15:18:25 +1300 Subject: [PATCH 04/10] Applied consistent changes to logs --- src/Sentry/SentryLog.cs | 55 +++++++++-------------------------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index 315b0a8a56..d40cd3e6ba 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -14,7 +14,7 @@ namespace Sentry; [DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")] public sealed class SentryLog { - private readonly Dictionary _attributes; + private readonly SentryAttributes _attributes; [SetsRequiredMembers] internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel level, string message) @@ -24,7 +24,7 @@ internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel le Level = level; Message = message; // 7 is the number of built-in attributes, so we start with that. - _attributes = new Dictionary(7); + _attributes = new SentryAttributes(7); } /// @@ -115,52 +115,22 @@ internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel le /// /// /// - public bool TryGetAttribute(string key, [NotNullWhen(true)] out object? value) - { - if (_attributes.TryGetValue(key, out var attribute) && attribute.Value is not null) - { - value = attribute.Value; - return true; - } - - value = null; - return false; - } + public bool TryGetAttribute(string key, [NotNullWhen(true)] out object? value) => + _attributes.TryGetAttribute(key, out value); - internal bool TryGetAttribute(string key, [NotNullWhen(true)] out string? value) - { - if (_attributes.TryGetValue(key, out var attribute) && attribute.Type == "string" && attribute.Value is not null) - { - value = (string)attribute.Value; - return true; - } - - value = null; - return false; - } + internal bool TryGetAttribute(string key, [NotNullWhen(true)] out string? value) => + _attributes.TryGetAttribute(key, out value); /// /// Set a key-value pair of data attached to the log. /// - public void SetAttribute(string key, object value) - { - _attributes[key] = new SentryAttribute(value); - } + public void SetAttribute(string key, object value) => _attributes.SetAttribute(key, value); - internal void SetAttribute(string key, string value) - { - _attributes[key] = new SentryAttribute(value, "string"); - } + internal void SetAttribute(string key, string value) => _attributes.SetAttribute(key, value); - internal void SetAttribute(string key, char value) - { - _attributes[key] = new SentryAttribute(value.ToString(), "string"); - } + internal void SetAttribute(string key, char value) => _attributes.SetAttribute(key, value); - internal void SetAttribute(string key, int value) - { - _attributes[key] = new SentryAttribute(value, "integer"); - } + internal void SetAttribute(string key, int value) => _attributes.SetAttribute(key, value); internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) { @@ -183,10 +153,7 @@ internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) } } - internal void SetOrigin(string origin) - { - SetAttribute("sentry.origin", origin); - } + internal void SetOrigin(string origin) => SetAttribute("sentry.origin", origin); internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { From ecf30ae78a88b8339ccbd7fdda85f1c860da4363 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 5 Mar 2026 16:18:32 +1300 Subject: [PATCH 05/10] . --- src/Sentry/SentryLog.cs | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index d40cd3e6ba..8bcae03511 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -132,26 +132,8 @@ internal bool TryGetAttribute(string key, [NotNullWhen(true)] out string? value) internal void SetAttribute(string key, int value) => _attributes.SetAttribute(key, value); - internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) - { - var environment = options.SettingLocator.GetEnvironment(); - SetAttribute("sentry.environment", environment); - - var release = options.SettingLocator.GetRelease(); - if (release is not null) - { - SetAttribute("sentry.release", release); - } - - if (sdk.Name is { } name) - { - SetAttribute("sentry.sdk.name", name); - } - if (sdk.Version is { } version) - { - SetAttribute("sentry.sdk.version", version); - } - } + internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) => + _attributes.SetDefaultAttributes(options, sdk); internal void SetOrigin(string origin) => SetAttribute("sentry.origin", origin); From c48cbd85dd848a6beb19554f425f6c3258513de9 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 12 May 2026 13:09:52 +1200 Subject: [PATCH 06/10] refactor(logs): expose SentryAttributes as property on SentryLog Replace the private `_attributes` field with an `internal SentryAttributes Attributes { get; }` property after `SpanId`, matching the `SentryMetric` pattern. Remove the internal typed overloads (`TryGetAttribute`, `SetAttribute(string/char/int)`) and update all callers to go through `log.Attributes` directly. Update tests to use `ShouldContain`/`ShouldNotContain` extension methods. Co-Authored-By: Claude Sonnet 4.6 --- .../SentryStructuredLogger.cs | 6 ++--- src/Sentry/SentryLog.cs | 25 ++++++------------- test/Sentry.Tests/SentryLogTests.cs | 17 +++++-------- .../SentryStructuredLoggerTests.cs | 3 +-- 4 files changed, 18 insertions(+), 33 deletions(-) diff --git a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs index e4f5b500d9..4b32e1b1b8 100644 --- a/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs +++ b/src/Sentry.Extensions.Logging/SentryStructuredLogger.cs @@ -92,15 +92,15 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except if (_categoryName is not null) { - log.SetAttribute("category.name", _categoryName); + log.Attributes.SetAttribute("category.name", _categoryName); } if (eventId.Name is not null || eventId.Id != 0) { - log.SetAttribute("event.id", eventId.Id); + log.Attributes.SetAttribute("event.id", eventId.Id); } if (eventId.Name is not null) { - log.SetAttribute("event.name", eventId.Name); + log.Attributes.SetAttribute("event.name", eventId.Name); } _hub.Logger.CaptureLog(log); diff --git a/src/Sentry/SentryLog.cs b/src/Sentry/SentryLog.cs index 35a7f89ca5..345e0e385a 100644 --- a/src/Sentry/SentryLog.cs +++ b/src/Sentry/SentryLog.cs @@ -14,8 +14,6 @@ namespace Sentry; [DebuggerDisplay(@"SentryLog \{ Level = {Level}, Message = '{Message}' \}")] public sealed class SentryLog { - private readonly SentryAttributes _attributes; - [SetsRequiredMembers] internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel level, string message) { @@ -24,7 +22,7 @@ internal SentryLog(DateTimeOffset timestamp, SentryId traceId, SentryLogLevel le Level = level; Message = message; // 7 is the number of built-in attributes, so we start with that. - _attributes = new SentryAttributes(7); + Attributes = new SentryAttributes(7); // ensure the ImmutableArray`1 is not default, so we can omit IsDefault checks before accessing other members Parameters = ImmutableArray>.Empty; } @@ -75,6 +73,8 @@ public ImmutableArray> Parameters /// public SpanId? SpanId { get; init; } + internal SentryAttributes Attributes { get; } + /// /// Gets the attribute value associated with the specified key. /// @@ -126,26 +126,17 @@ public ImmutableArray> Parameters /// /// public bool TryGetAttribute(string key, [NotNullWhen(true)] out object? value) => - _attributes.TryGetAttribute(key, out value); - - internal bool TryGetAttribute(string key, [NotNullWhen(true)] out string? value) => - _attributes.TryGetAttribute(key, out value); + Attributes.TryGetAttribute(key, out value); /// /// Set a key-value pair of data attached to the log. /// - public void SetAttribute(string key, object value) => _attributes.SetAttribute(key, value); - - internal void SetAttribute(string key, string value) => _attributes.SetAttribute(key, value); - - internal void SetAttribute(string key, char value) => _attributes.SetAttribute(key, value); - - internal void SetAttribute(string key, int value) => _attributes.SetAttribute(key, value); + public void SetAttribute(string key, object value) => Attributes.SetAttribute(key, value); internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk) => - _attributes.SetDefaultAttributes(options, sdk); + Attributes.SetDefaultAttributes(options, sdk); - internal void SetOrigin(string origin) => SetAttribute("sentry.origin", origin); + internal void SetOrigin(string origin) => Attributes.SetAttribute("sentry.origin", origin); internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { @@ -193,7 +184,7 @@ internal void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) SentryAttributeSerializer.WriteAttribute(writer, $"sentry.message.parameter.{parameter.Key}", parameter.Value, logger); } - foreach (var attribute in _attributes) + foreach (var attribute in Attributes) { SentryAttributeSerializer.WriteAttribute(writer, attribute.Key, attribute.Value, logger); } diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index 5abc5664fc..a45c699741 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -68,18 +68,14 @@ public void Protocol_Default_VerifyAttributes() log.SpanId.Should().Be(SpanId); // should only show up in sdk integrations - log.TryGetAttribute("sentry.origin", out object origin).Should().BeFalse(); + log.Attributes.ShouldNotContain("sentry.origin"); log.TryGetAttribute("attribute", out object attribute).Should().BeTrue(); attribute.Should().Be("value"); - log.TryGetAttribute("sentry.environment", out string environment).Should().BeTrue(); - environment.Should().Be(options.Environment); - log.TryGetAttribute("sentry.release", out string release).Should().BeTrue(); - release.Should().Be(options.Release); - log.TryGetAttribute("sentry.sdk.name", out string name).Should().BeTrue(); - name.Should().Be(sdk.Name); - log.TryGetAttribute("sentry.sdk.version", out string version).Should().BeTrue(); - version.Should().Be(sdk.Version); + log.Attributes.ShouldContain("sentry.environment", options.Environment!); + log.Attributes.ShouldContain("sentry.release", options.Release!); + log.Attributes.ShouldContain("sentry.sdk.name", sdk.Name!); + log.Attributes.ShouldContain("sentry.sdk.version", sdk.Version!); log.TryGetAttribute("not-found", out object notFound).Should().BeFalse(); notFound.Should().BeNull(); } @@ -416,8 +412,7 @@ public void WriteTo_Attributes_AsJson() entry => entry.Message.Should().Match("*nuint*is not supported*64-bit*"), #endif entry => entry.Message.Should().Match("*decimal*is not supported*overflow*"), - entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*"), - entry => entry.Message.Should().Match("*null*is not supported*ignored*") + entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*") ); } } diff --git a/test/Sentry.Tests/SentryStructuredLoggerTests.cs b/test/Sentry.Tests/SentryStructuredLoggerTests.cs index 3fbca3197a..b214ea0ab4 100644 --- a/test/Sentry.Tests/SentryStructuredLoggerTests.cs +++ b/test/Sentry.Tests/SentryStructuredLoggerTests.cs @@ -292,8 +292,7 @@ public static void AssertLog(this SentryStructuredLoggerTests.Fixture fixture, S foreach (var expectedAttribute in fixture.ExpectedAttributes) { - log.TryGetAttribute(expectedAttribute.Key, out string? value).Should().BeTrue(); - value.Should().Be(expectedAttribute.Value); + log.Attributes.ShouldContain(expectedAttribute.Key, expectedAttribute.Value); } } From 2ed634f16c747da6b31f3517b00bdbe67f895946 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 12 May 2026 13:13:43 +1200 Subject: [PATCH 07/10] docs(logs): remove SentryMetric-specific references from SentryAttributes Co-Authored-By: Claude Sonnet 4.6 --- src/Sentry/Protocol/SentryAttributes.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sentry/Protocol/SentryAttributes.cs b/src/Sentry/Protocol/SentryAttributes.cs index 211a3591c5..5d975373cb 100644 --- a/src/Sentry/Protocol/SentryAttributes.cs +++ b/src/Sentry/Protocol/SentryAttributes.cs @@ -16,7 +16,7 @@ public SentryAttributes(int capacity) : base(capacity, StringComparer.Ordinal) /// Gets the attribute value associated with the specified key. /// /// - /// Returns if this contains an attribute with the specified key which is of type and it's value is not . + /// Returns if this contains an attribute with the specified key which is of type and it's value is not . /// Otherwise . /// Supported types: /// @@ -61,7 +61,7 @@ public SentryAttributes(int capacity) : base(capacity, StringComparer.Ordinal) /// /// /// - /// + /// public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out TAttribute value) { if (TryGetValue(key, out var attribute) && attribute.Value is TAttribute attributeValue) @@ -75,7 +75,7 @@ public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out T } /// - /// Set a key-value pair of data attached to the metric. + /// Set a key-value pair of data attached to the attribute collection. /// public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { From bd885f8f5e77de76bf2c006dab88564214f09247 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 12 May 2026 13:20:23 +1200 Subject: [PATCH 08/10] test(metrics): use public API and correct type in SentryMetricTests Change ShouldNotContain to ShouldNotContain to match previous semantics, and replace Attributes.SetAttribute("null", null!) with the public metric.SetAttribute("null", null!) overload. Remove the now-unreachable null warning expectation. Co-Authored-By: Claude Sonnet 4.6 --- test/Sentry.Tests/SentryMetricTests.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/Sentry.Tests/SentryMetricTests.cs b/test/Sentry.Tests/SentryMetricTests.cs index d37670def4..16fa662b66 100644 --- a/test/Sentry.Tests/SentryMetricTests.cs +++ b/test/Sentry.Tests/SentryMetricTests.cs @@ -71,7 +71,7 @@ public void Protocol_Default_VerifyAttributes() metric.Attributes.ShouldContain("sentry.release", options.Release); metric.Attributes.ShouldContain("sentry.sdk.name", sdk.Name); metric.Attributes.ShouldContain("sentry.sdk.version", sdk.Version); - metric.Attributes.ShouldNotContain("not-found"); + metric.Attributes.ShouldNotContain("not-found"); } [Fact] @@ -368,7 +368,7 @@ public void WriteTo_Attributes_AsJson() #else metric.SetAttribute("object", new KeyValuePair("key", "value")); #endif - metric.Attributes.SetAttribute("null", null!); + metric.SetAttribute("null", null!); var document = metric.ToJsonDocument(static (obj, writer, logger) => obj.WriteTo(writer, logger), _output); var attributes = document.RootElement.GetProperty("attributes"); @@ -399,8 +399,7 @@ public void WriteTo_Attributes_AsJson() entry => entry.Message.Should().Match("*nuint*is not supported*64-bit*"), #endif entry => entry.Message.Should().Match("*decimal*is not supported*overflow*"), - entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*"), - entry => entry.Message.Should().Match("*null*is not supported*ignored*") + entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*") ); } } From 7d0a86025aff7ab9e156d0efb350bd11f74b0c06 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 12 May 2026 13:48:39 +1200 Subject: [PATCH 09/10] fix(attributes): emit diagnostic log when null attribute is set Remove the early null-return from SentryAttributes.SetAttribute so null values are stored in the collection and the existing null warning in SentryAttributeSerializer fires consistently during serialization for both SentryLog and SentryMetric. Co-Authored-By: Claude Sonnet 4.6 --- src/Sentry/Protocol/SentryAttributes.cs | 7 +------ test/Sentry.Tests/SentryLogTests.cs | 3 ++- test/Sentry.Tests/SentryMetricTests.cs | 3 ++- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Sentry/Protocol/SentryAttributes.cs b/src/Sentry/Protocol/SentryAttributes.cs index 5d975373cb..761db87240 100644 --- a/src/Sentry/Protocol/SentryAttributes.cs +++ b/src/Sentry/Protocol/SentryAttributes.cs @@ -79,12 +79,7 @@ public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out T /// public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { - if (value is null) - { - return; - } - - this[key] = new SentryAttribute(value); + this[key] = new SentryAttribute(value!); } internal void SetAttribute(string key, string value) diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index a45c699741..2c3688966e 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -412,7 +412,8 @@ public void WriteTo_Attributes_AsJson() entry => entry.Message.Should().Match("*nuint*is not supported*64-bit*"), #endif entry => entry.Message.Should().Match("*decimal*is not supported*overflow*"), - entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*") + entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*"), + entry => entry.Message.Should().Match("*null*is not supported*ignored*") ); } } diff --git a/test/Sentry.Tests/SentryMetricTests.cs b/test/Sentry.Tests/SentryMetricTests.cs index 16fa662b66..d369324cb7 100644 --- a/test/Sentry.Tests/SentryMetricTests.cs +++ b/test/Sentry.Tests/SentryMetricTests.cs @@ -399,7 +399,8 @@ public void WriteTo_Attributes_AsJson() entry => entry.Message.Should().Match("*nuint*is not supported*64-bit*"), #endif entry => entry.Message.Should().Match("*decimal*is not supported*overflow*"), - entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*") + entry => entry.Message.Should().Match("*System.Collections.Generic.KeyValuePair`2[System.String,System.String]*is not supported*ToString*"), + entry => entry.Message.Should().Match("*null*is not supported*ignored*") ); } } From 7699aaa5f7dd8edc0da3c83639d4b078d3ed97bc Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 13 May 2026 11:27:29 +1200 Subject: [PATCH 10/10] Apply review suggestions: doc wording and test assertion consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SentryAttributes.TryGetAttribute: "this" → "this attribute collection" - SentryAttributes.SetAttribute: rephrase summary doc - SentryAttributes.SetAttribute: remove unnecessary null-suppression operator - SentryAttributes.TryGetAttribute: update seealso href to attributes docs - SentryLogTests: use ShouldContain/ShouldNotContain instead of TryGetAttribute - SentryLogTests: remove unnecessary null-forgiving operators Co-Authored-By: Claude Sonnet 4.6 --- src/Sentry/Protocol/SentryAttributes.cs | 8 ++++---- test/Sentry.Tests/SentryLogTests.cs | 14 ++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Sentry/Protocol/SentryAttributes.cs b/src/Sentry/Protocol/SentryAttributes.cs index 761db87240..711860e480 100644 --- a/src/Sentry/Protocol/SentryAttributes.cs +++ b/src/Sentry/Protocol/SentryAttributes.cs @@ -16,7 +16,7 @@ public SentryAttributes(int capacity) : base(capacity, StringComparer.Ordinal) /// Gets the attribute value associated with the specified key. /// /// - /// Returns if this contains an attribute with the specified key which is of type and it's value is not . + /// Returns if this attribute collection contains an attribute with the specified key which is of type and it's value is not . /// Otherwise . /// Supported types: /// @@ -61,7 +61,7 @@ public SentryAttributes(int capacity) : base(capacity, StringComparer.Ordinal) /// /// /// - /// + /// public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out TAttribute value) { if (TryGetValue(key, out var attribute) && attribute.Value is TAttribute attributeValue) @@ -75,11 +75,11 @@ public bool TryGetAttribute(string key, [MaybeNullWhen(false)] out T } /// - /// Set a key-value pair of data attached to the attribute collection. + /// Set a key-value pair in this attribute collection. /// public void SetAttribute(string key, TAttribute value) where TAttribute : notnull { - this[key] = new SentryAttribute(value!); + this[key] = new SentryAttribute(value); } internal void SetAttribute(string key, string value) diff --git a/test/Sentry.Tests/SentryLogTests.cs b/test/Sentry.Tests/SentryLogTests.cs index 2c3688966e..b3ed1fc43b 100644 --- a/test/Sentry.Tests/SentryLogTests.cs +++ b/test/Sentry.Tests/SentryLogTests.cs @@ -70,14 +70,12 @@ public void Protocol_Default_VerifyAttributes() // should only show up in sdk integrations log.Attributes.ShouldNotContain("sentry.origin"); - log.TryGetAttribute("attribute", out object attribute).Should().BeTrue(); - attribute.Should().Be("value"); - log.Attributes.ShouldContain("sentry.environment", options.Environment!); - log.Attributes.ShouldContain("sentry.release", options.Release!); - log.Attributes.ShouldContain("sentry.sdk.name", sdk.Name!); - log.Attributes.ShouldContain("sentry.sdk.version", sdk.Version!); - log.TryGetAttribute("not-found", out object notFound).Should().BeFalse(); - notFound.Should().BeNull(); + log.Attributes.ShouldContain("attribute", "value"); + log.Attributes.ShouldContain("sentry.environment", options.Environment); + log.Attributes.ShouldContain("sentry.release", options.Release); + log.Attributes.ShouldContain("sentry.sdk.name", sdk.Name); + log.Attributes.ShouldContain("sentry.sdk.version", sdk.Version); + log.Attributes.ShouldNotContain("not-found"); } [Theory]