From 0add668b6821a7c5fdc2116c31ee2f2f2db3cba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 30 Apr 2025 13:09:47 +0200 Subject: [PATCH 1/4] feat(logs): initial experiment --- .../Sentry.Samples.Console.Basic/Program.cs | 12 +++ .../Experimental/SentryExperimentalSdk.cs | 20 ++++ .../Experimental/SentryHubExtensions.cs | 15 +++ src/Sentry/Experimental/SentryLog.cs | 102 ++++++++++++++++++ src/Sentry/Experimental/SentrySeverity.cs | 34 ++++++ ...tics.CodeAnalysis.ExperimentalAttribute.cs | 29 +++++ src/Sentry/Infrastructure/DiagnosticId.cs | 2 + src/Sentry/Protocol/Envelopes/Envelope.cs | 14 +++ src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 16 +++ 9 files changed, 244 insertions(+) create mode 100644 src/Sentry/Experimental/SentryExperimentalSdk.cs create mode 100644 src/Sentry/Experimental/SentryHubExtensions.cs create mode 100644 src/Sentry/Experimental/SentryLog.cs create mode 100644 src/Sentry/Experimental/SentrySeverity.cs create mode 100644 src/Sentry/Experimental/System.Diagnostics.CodeAnalysis.ExperimentalAttribute.cs diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs index be18640323..4dd36556a2 100644 --- a/samples/Sentry.Samples.Console.Basic/Program.cs +++ b/samples/Sentry.Samples.Console.Basic/Program.cs @@ -9,6 +9,7 @@ */ using System.Net.Http; +using Sentry.Experimental; using static System.Console; // Initialize the Sentry SDK. (It is not necessary to dispose it.) @@ -35,6 +36,16 @@ options.TracesSampleRate = 1.0; }); +#pragma warning disable SENTRY0002 +SentryExperimentalSdk.CaptureLog(SentrySeverity.Trace, "Hello, World!"); +SentryExperimentalSdk.CaptureLog(SentrySeverity.Debug, "Hello, .NET!"); +SentryExperimentalSdk.CaptureLog(SentrySeverity.Info, "Information"); +SentryExperimentalSdk.CaptureLog(SentrySeverity.Warn, "Warning with one {0}", "parameter"); +SentryExperimentalSdk.CaptureLog(SentrySeverity.Error, "Error with {0} {1}", 2, "parameters"); +SentryExperimentalSdk.CaptureLog(SentrySeverity.Fatal, "Fatal {0} and {1}", true, false); +#pragma warning restore SENTRY0002 + +/* // This starts a new transaction and attaches it to the scope. var transaction = SentrySdk.StartTransaction("Program Main", "function"); SentrySdk.ConfigureScope(scope => scope.Transaction = transaction); @@ -96,3 +107,4 @@ async Task ThirdFunction() span.Finish(); } } +*/ diff --git a/src/Sentry/Experimental/SentryExperimentalSdk.cs b/src/Sentry/Experimental/SentryExperimentalSdk.cs new file mode 100644 index 0000000000..a08bb073d8 --- /dev/null +++ b/src/Sentry/Experimental/SentryExperimentalSdk.cs @@ -0,0 +1,20 @@ +using Sentry.Infrastructure; + +namespace Sentry.Experimental; + +/// +/// Experimental Sentry SDK entrypoint. +/// +public static class SentryExperimentalSdk +{ + /// + /// See: https://github.com/getsentry/sentry-dotnet/issues/4132 + /// + [Experimental(DiagnosticId.ExperimentalSentryLogs, UrlFormat = "https://github.com/getsentry/sentry-dotnet/issues/4132")] + public static void CaptureLog(SentrySeverity level, string template, params object[]? parameters) + { + string message = String.Format(template, parameters ?? []); + SentryLog log = new(level, message); + _ = SentrySdk.CurrentHub.CaptureLog(log); + } +} diff --git a/src/Sentry/Experimental/SentryHubExtensions.cs b/src/Sentry/Experimental/SentryHubExtensions.cs new file mode 100644 index 0000000000..fd89211250 --- /dev/null +++ b/src/Sentry/Experimental/SentryHubExtensions.cs @@ -0,0 +1,15 @@ +using Sentry.Infrastructure; +using Sentry.Protocol.Envelopes; + +namespace Sentry.Experimental; + +internal static class SentryHubExtensions +{ + [Experimental(DiagnosticId.ExperimentalSentryLogs)] + internal static int CaptureLog(this IHub hub, SentryLog log) + { + _ = hub.CaptureEnvelope(Envelope.FromLog(log)); + + return default; + } +} diff --git a/src/Sentry/Experimental/SentryLog.cs b/src/Sentry/Experimental/SentryLog.cs new file mode 100644 index 0000000000..f46dacf397 --- /dev/null +++ b/src/Sentry/Experimental/SentryLog.cs @@ -0,0 +1,102 @@ +using Sentry.Extensibility; +using Sentry.Infrastructure; +using Sentry.Internal.Extensions; + +namespace Sentry.Experimental; + +[Experimental(DiagnosticId.ExperimentalSentryLogs)] +internal sealed class SentryLog : ISentryJsonSerializable +{ + [SetsRequiredMembers] + public SentryLog(SentrySeverity level, string message, object[]? parameters = null) + { + Timestamp = DateTimeOffset.UtcNow; + TraceId = SentryId.Empty; + Level = level; + Message = message; + Parameters = parameters; + } + + public required DateTimeOffset Timestamp { get; init; } + + public required SentryId TraceId { get; init; } + + public required SentrySeverity Level { get; init; } + + public required string Message { get; init; } + + public Dictionary? Attributes { get; private set; } + + public string? Template { get; init; } + + public object[]? Parameters { get; init; } + + public int SeverityNumber { get; init; } = -1; + + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + Attributes = new Dictionary + { + //{ "sentry.environment", new ValueTypePair("production", "string")}, + //{ "sentry.release", new ValueTypePair("1.0.0", "string")}, + //{ "sentry.trace.parent_span_id", new ValueTypePair("b0e6f15b45c36b12", "string")}, + }; + if (Template is not null) + { + Attributes["sentry.message.template"] = new ValueTypePair("User %s has logged in!", "string"); + } + + if (Parameters is not null) + { + for (var index = 0; index < Parameters.Length; index++) + { + Attributes[$"sentry.message.parameters.{index}"] = new ValueTypePair(Parameters[index], "string"); + } + } + + writer.WriteStartObject(); + + writer.WriteStartArray("items"); + + writer.WriteStartObject(); + + writer.WriteNumber("timestamp", Timestamp.ToUnixTimeSeconds()); + writer.WriteString("trace_id", TraceId); + writer.WriteString("level", Level.ToLogString()); + writer.WriteString("body", Message); + writer.WriteDictionaryIfNotEmpty("attributes", Attributes, logger); + + if (SeverityNumber != -1) + { + writer.WriteNumber("severity_number", SeverityNumber); + } + + writer.WriteEndObject(); + + writer.WriteEndArray(); + + writer.WriteEndObject(); + } +} + +internal readonly struct ValueTypePair : ISentryJsonSerializable +{ + public ValueTypePair(object value, string type) + { + Value = value.ToString()!; + Type = type; + } + + public string Value { get; } + public string Type { get; } + + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + + writer.WriteString("value", Value); + writer.WriteString("type", Type); + + writer.WriteEndObject(); + } +} diff --git a/src/Sentry/Experimental/SentrySeverity.cs b/src/Sentry/Experimental/SentrySeverity.cs new file mode 100644 index 0000000000..92bcdd5c46 --- /dev/null +++ b/src/Sentry/Experimental/SentrySeverity.cs @@ -0,0 +1,34 @@ +using Sentry.Infrastructure; + +namespace Sentry.Experimental; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +[Experimental(DiagnosticId.ExperimentalSentryLogs)] +public enum SentrySeverity : short +{ + Trace, + Debug, + Info, + Warn, + Error, + Fatal, +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + +[Experimental(DiagnosticId.ExperimentalSentryLogs)] +internal static class SentrySeverityExtensions +{ + public static string ToLogString(this SentrySeverity severity) + { + return severity switch + { + SentrySeverity.Trace => "trace", + SentrySeverity.Debug => "debug", + SentrySeverity.Info => "info", + SentrySeverity.Warn => "warn", + SentrySeverity.Error => "error", + SentrySeverity.Fatal => "fatal", + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null), + }; + } +} diff --git a/src/Sentry/Experimental/System.Diagnostics.CodeAnalysis.ExperimentalAttribute.cs b/src/Sentry/Experimental/System.Diagnostics.CodeAnalysis.ExperimentalAttribute.cs new file mode 100644 index 0000000000..b173d83323 --- /dev/null +++ b/src/Sentry/Experimental/System.Diagnostics.CodeAnalysis.ExperimentalAttribute.cs @@ -0,0 +1,29 @@ +#if !NET8_0_OR_GREATER +// ReSharper disable CheckNamespace +// ReSharper disable ConvertToPrimaryConstructor +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Assembly | + AttributeTargets.Module | + AttributeTargets.Class | + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Constructor | + AttributeTargets.Method | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Event | + AttributeTargets.Interface | + AttributeTargets.Delegate, Inherited = false)] +internal sealed class ExperimentalAttribute : Attribute +{ + public ExperimentalAttribute(string diagnosticId) + { + DiagnosticId = diagnosticId; + } + + public string DiagnosticId { get; } + + public string? UrlFormat { get; set; } +} +#endif diff --git a/src/Sentry/Infrastructure/DiagnosticId.cs b/src/Sentry/Infrastructure/DiagnosticId.cs index 92703ddc87..ebd17b51d2 100644 --- a/src/Sentry/Infrastructure/DiagnosticId.cs +++ b/src/Sentry/Infrastructure/DiagnosticId.cs @@ -8,4 +8,6 @@ internal static class DiagnosticId /// internal const string ExperimentalFeature = "SENTRY0001"; #endif + + internal const string ExperimentalSentryLogs = "SENTRY0002"; } diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index b62dc82c98..0fb07b47d2 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -1,3 +1,4 @@ +using Sentry.Experimental; using Sentry.Extensibility; using Sentry.Infrastructure; using Sentry.Internal; @@ -445,6 +446,19 @@ internal static Envelope FromClientReport(ClientReport clientReport) return new Envelope(header, items); } + [Experimental(DiagnosticId.ExperimentalSentryLogs)] + internal static Envelope FromLog(SentryLog log) + { + var header = DefaultHeader; + + var items = new[] + { + EnvelopeItem.FromLog(log) + }; + + return new Envelope(header, items); + } + private static async Task> DeserializeHeaderAsync( Stream stream, CancellationToken cancellationToken = default) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 7c721db581..a73f9e4ab8 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -1,4 +1,6 @@ +using Sentry.Experimental; using Sentry.Extensibility; +using Sentry.Infrastructure; using Sentry.Internal; using Sentry.Internal.Extensions; using Sentry.Protocol.Metrics; @@ -24,6 +26,7 @@ public sealed class EnvelopeItem : ISerializable, IDisposable internal const string TypeValueProfile = "profile"; internal const string TypeValueMetric = "statsd"; internal const string TypeValueCodeLocations = "metric_meta"; + internal const string TypeValueLog = "log"; private const string LengthKey = "length"; private const string FileNameKey = "filename"; @@ -370,6 +373,19 @@ internal static EnvelopeItem FromClientReport(ClientReport report) return new EnvelopeItem(header, new JsonSerializable(report)); } + [Experimental(DiagnosticId.ExperimentalSentryLogs)] + internal static EnvelopeItem FromLog(SentryLog log) + { + var header = new Dictionary(3, StringComparer.Ordinal) + { + [TypeKey] = TypeValueLog, + ["item_count"] = 1, + ["content_type"] = "application/vnd.sentry.items.log+json", + }; + + return new EnvelopeItem(header, new JsonSerializable(log)); + } + private static async Task> DeserializeHeaderAsync( Stream stream, CancellationToken cancellationToken = default) From db7d558081582ebf2802978b60de6eba6be29a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Wed, 30 Apr 2025 18:06:38 +0200 Subject: [PATCH 2/4] feat(logs): basic Logger Module API shape --- .../Sentry.Samples.Console.Basic/Program.cs | 14 +-- .../Experimental/SentryExperimentalSdk.cs | 20 ---- .../Experimental/SentryHubExtensions.cs | 15 --- src/Sentry/Experimental/SentryLog.cs | 99 ++++++++++++++----- src/Sentry/Experimental/SentrySeverity.cs | 46 ++++++++- ...System.Diagnostics.UnreachableException.cs | 22 +++++ src/Sentry/Infrastructure/DiagnosticId.cs | 7 ++ .../Internal/Extensions/JsonExtensions.cs | 11 +++ src/Sentry/SentryLogger.cs | 68 +++++++++++++ src/Sentry/SentrySdk.cs | 9 ++ .../SqlListenerTests.verify.cs | 2 +- 11 files changed, 245 insertions(+), 68 deletions(-) delete mode 100644 src/Sentry/Experimental/SentryExperimentalSdk.cs delete mode 100644 src/Sentry/Experimental/SentryHubExtensions.cs create mode 100644 src/Sentry/Experimental/System.Diagnostics.UnreachableException.cs create mode 100644 src/Sentry/SentryLogger.cs diff --git a/samples/Sentry.Samples.Console.Basic/Program.cs b/samples/Sentry.Samples.Console.Basic/Program.cs index 4dd36556a2..7fab354823 100644 --- a/samples/Sentry.Samples.Console.Basic/Program.cs +++ b/samples/Sentry.Samples.Console.Basic/Program.cs @@ -37,14 +37,16 @@ }); #pragma warning disable SENTRY0002 -SentryExperimentalSdk.CaptureLog(SentrySeverity.Trace, "Hello, World!"); -SentryExperimentalSdk.CaptureLog(SentrySeverity.Debug, "Hello, .NET!"); -SentryExperimentalSdk.CaptureLog(SentrySeverity.Info, "Information"); -SentryExperimentalSdk.CaptureLog(SentrySeverity.Warn, "Warning with one {0}", "parameter"); -SentryExperimentalSdk.CaptureLog(SentrySeverity.Error, "Error with {0} {1}", 2, "parameters"); -SentryExperimentalSdk.CaptureLog(SentrySeverity.Fatal, "Fatal {0} and {1}", true, false); +SentrySdk.Logger.Trace("Hello, World!", null, log => log.SetAttribute("trace", "trace")); +SentrySdk.Logger.Debug("Hello, .NET!", null, log => log.SetAttribute("trace", "trace")); +SentrySdk.Logger.Info("Information", null, log => log.SetAttribute("trace", "trace")); +SentrySdk.Logger.Warn("Warning with one {0}", ["parameter"], log => log.SetAttribute("trace", "trace")); +SentrySdk.Logger.Error("Error with {0} {1}", [2, "parameters"], log => log.SetAttribute("trace", "trace")); +SentrySdk.Logger.Fatal("Fatal {0} and {1}", [true, false], log => log.SetAttribute("trace", "trace")); #pragma warning restore SENTRY0002 +await Task.Delay(5_000); + /* // This starts a new transaction and attaches it to the scope. var transaction = SentrySdk.StartTransaction("Program Main", "function"); diff --git a/src/Sentry/Experimental/SentryExperimentalSdk.cs b/src/Sentry/Experimental/SentryExperimentalSdk.cs deleted file mode 100644 index a08bb073d8..0000000000 --- a/src/Sentry/Experimental/SentryExperimentalSdk.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Sentry.Infrastructure; - -namespace Sentry.Experimental; - -/// -/// Experimental Sentry SDK entrypoint. -/// -public static class SentryExperimentalSdk -{ - /// - /// See: https://github.com/getsentry/sentry-dotnet/issues/4132 - /// - [Experimental(DiagnosticId.ExperimentalSentryLogs, UrlFormat = "https://github.com/getsentry/sentry-dotnet/issues/4132")] - public static void CaptureLog(SentrySeverity level, string template, params object[]? parameters) - { - string message = String.Format(template, parameters ?? []); - SentryLog log = new(level, message); - _ = SentrySdk.CurrentHub.CaptureLog(log); - } -} diff --git a/src/Sentry/Experimental/SentryHubExtensions.cs b/src/Sentry/Experimental/SentryHubExtensions.cs deleted file mode 100644 index fd89211250..0000000000 --- a/src/Sentry/Experimental/SentryHubExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Sentry.Infrastructure; -using Sentry.Protocol.Envelopes; - -namespace Sentry.Experimental; - -internal static class SentryHubExtensions -{ - [Experimental(DiagnosticId.ExperimentalSentryLogs)] - internal static int CaptureLog(this IHub hub, SentryLog log) - { - _ = hub.CaptureEnvelope(Envelope.FromLog(log)); - - return default; - } -} diff --git a/src/Sentry/Experimental/SentryLog.cs b/src/Sentry/Experimental/SentryLog.cs index f46dacf397..fde5a3106f 100644 --- a/src/Sentry/Experimental/SentryLog.cs +++ b/src/Sentry/Experimental/SentryLog.cs @@ -2,13 +2,18 @@ using Sentry.Infrastructure; using Sentry.Internal.Extensions; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + namespace Sentry.Experimental; [Experimental(DiagnosticId.ExperimentalSentryLogs)] -internal sealed class SentryLog : ISentryJsonSerializable +public sealed class SentryLog : ISentryJsonSerializable { + private Dictionary? _attributes; + private int _severityNumber = -1; + [SetsRequiredMembers] - public SentryLog(SentrySeverity level, string message, object[]? parameters = null) + internal SentryLog(SentrySeverity level, string message, object[]? parameters = null) { Timestamp = DateTimeOffset.UtcNow; TraceId = SentryId.Empty; @@ -21,50 +26,99 @@ public SentryLog(SentrySeverity level, string message, object[]? parameters = nu public required SentryId TraceId { get; init; } - public required SentrySeverity Level { get; init; } + public SentrySeverity Level + { + get => SentrySeverityExtensions.FromSeverityNumber(_severityNumber); + set => _severityNumber = SentrySeverityExtensions.ToSeverityNumber(value); + } public required string Message { get; init; } - public Dictionary? Attributes { get; private set; } + //public Dictionary? Attributes { get { return _attributes; } } public string? Template { get; init; } public object[]? Parameters { get; init; } - public int SeverityNumber { get; init; } = -1; + public required int SeverityNumber + { + get => _severityNumber; + set + { + // + SentrySeverityExtensions.ThrowIfOutOfRange(value); + _severityNumber = value; + } + } + + public void SetAttribute(string key, string value) + { + _attributes ??= new Dictionary(); + _attributes[key] = new ValueTypePair(value, "string"); + } + + public void SetAttribute(string key, bool value) + { + _attributes ??= new Dictionary(); + _attributes[key] = new ValueTypePair(value, "boolean"); + } + + public void SetAttribute(string key, int value) + { + _attributes ??= new Dictionary(); + _attributes[key] = new ValueTypePair(value, "integer"); + } + + public void SetAttribute(string key, double value) + { + _attributes ??= new Dictionary(); + _attributes[key] = new ValueTypePair(value, "double"); + } public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) { - Attributes = new Dictionary + _attributes = new Dictionary { - //{ "sentry.environment", new ValueTypePair("production", "string")}, - //{ "sentry.release", new ValueTypePair("1.0.0", "string")}, - //{ "sentry.trace.parent_span_id", new ValueTypePair("b0e6f15b45c36b12", "string")}, + { "sentry.environment", new ValueTypePair("production", "string")}, + { "sentry.release", new ValueTypePair("1.0.0", "string")}, + { "sentry.trace.parent_span_id", new ValueTypePair("b0e6f15b45c36b12", "string")}, }; + + writer.WriteStartObject(); + writer.WriteStartArray("items"); + writer.WriteStartObject(); + + writer.WriteNumber("timestamp", Timestamp.ToUnixTimeSeconds()); + writer.WriteString("trace_id", TraceId); + writer.WriteString("level", Level.ToLogString()); + writer.WriteString("body", Message); + + writer.WritePropertyName("attributes"); + writer.WriteStartObject(); + if (Template is not null) { - Attributes["sentry.message.template"] = new ValueTypePair("User %s has logged in!", "string"); + writer.WriteSerializable("sentry.message.template", new ValueTypePair(Template, "string"), null); } if (Parameters is not null) { for (var index = 0; index < Parameters.Length; index++) { - Attributes[$"sentry.message.parameters.{index}"] = new ValueTypePair(Parameters[index], "string"); + var type = "string"; + writer.WriteSerializable($"sentry.message.parameters.{index}", new ValueTypePair(Parameters[index], type), null); } } - writer.WriteStartObject(); - - writer.WriteStartArray("items"); - - writer.WriteStartObject(); + if (_attributes is not null) + { + foreach (var attribute in _attributes) + { + writer.WriteSerializable(attribute.Key, attribute.Value, null); + } + } - writer.WriteNumber("timestamp", Timestamp.ToUnixTimeSeconds()); - writer.WriteString("trace_id", TraceId); - writer.WriteString("level", Level.ToLogString()); - writer.WriteString("body", Message); - writer.WriteDictionaryIfNotEmpty("attributes", Attributes, logger); + writer.WriteEndObject(); if (SeverityNumber != -1) { @@ -72,13 +126,12 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) } writer.WriteEndObject(); - writer.WriteEndArray(); - writer.WriteEndObject(); } } +//TODO: remove? perhaps a simple System.ValueTuple`2 suffices internal readonly struct ValueTypePair : ISentryJsonSerializable { public ValueTypePair(object value, string type) diff --git a/src/Sentry/Experimental/SentrySeverity.cs b/src/Sentry/Experimental/SentrySeverity.cs index 92bcdd5c46..189d8eac6c 100644 --- a/src/Sentry/Experimental/SentrySeverity.cs +++ b/src/Sentry/Experimental/SentrySeverity.cs @@ -1,8 +1,11 @@ using Sentry.Infrastructure; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + namespace Sentry.Experimental; -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +//TODO: QUESTION: not sure about the name +// this is a bit different to Sentry.SentryLevel and Sentry.BreadcrumbLevel [Experimental(DiagnosticId.ExperimentalSentryLogs)] public enum SentrySeverity : short { @@ -13,12 +16,11 @@ public enum SentrySeverity : short Error, Fatal, } -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member [Experimental(DiagnosticId.ExperimentalSentryLogs)] internal static class SentrySeverityExtensions { - public static string ToLogString(this SentrySeverity severity) + internal static string ToLogString(this SentrySeverity severity) { return severity switch { @@ -31,4 +33,42 @@ public static string ToLogString(this SentrySeverity severity) _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null), }; } + + internal static SentrySeverity FromSeverityNumber(int severityNumber) + { + ThrowIfOutOfRange(severityNumber); + + return severityNumber switch + { + >= 1 and <= 4 => SentrySeverity.Trace, + >= 5 and <= 8 => SentrySeverity.Debug, + >= 9 and <= 12 => SentrySeverity.Info, + >= 13 and <= 16 => SentrySeverity.Warn, + >= 17 and <= 20 => SentrySeverity.Error, + >= 21 and <= 24 => SentrySeverity.Fatal, + _ => throw new UnreachableException(), + }; + } + + internal static int ToSeverityNumber(SentrySeverity severity) + { + return severity switch + { + SentrySeverity.Trace => 1, + SentrySeverity.Debug => 5, + SentrySeverity.Info => 9, + SentrySeverity.Warn => 13, + SentrySeverity.Error => 17, + SentrySeverity.Fatal => 21, + _ => throw new ArgumentOutOfRangeException(nameof(severity), severity, null) + }; + } + + internal static void ThrowIfOutOfRange(int severityNumber) + { + if (severityNumber is < 1 or > 24) + { + throw new ArgumentOutOfRangeException(nameof(severityNumber), severityNumber, "SeverityNumber must be between 1 (inclusive) and 24 (inclusive)."); + } + } } diff --git a/src/Sentry/Experimental/System.Diagnostics.UnreachableException.cs b/src/Sentry/Experimental/System.Diagnostics.UnreachableException.cs new file mode 100644 index 0000000000..48b51df92e --- /dev/null +++ b/src/Sentry/Experimental/System.Diagnostics.UnreachableException.cs @@ -0,0 +1,22 @@ +#if !NET7_0_OR_GREATER +// ReSharper disable CheckNamespace +namespace System.Diagnostics; + +internal sealed class UnreachableException : Exception +{ + public UnreachableException() + : base("The program executed an instruction that was thought to be unreachable.") + { + } + + public UnreachableException(string? message) + : base(message ?? "The program executed an instruction that was thought to be unreachable.") + { + } + + public UnreachableException(string? message, Exception? innerException) + : base(message ?? "The program executed an instruction that was thought to be unreachable.", innerException) + { + } +} +#endif diff --git a/src/Sentry/Infrastructure/DiagnosticId.cs b/src/Sentry/Infrastructure/DiagnosticId.cs index ebd17b51d2..b85f489414 100644 --- a/src/Sentry/Infrastructure/DiagnosticId.cs +++ b/src/Sentry/Infrastructure/DiagnosticId.cs @@ -9,5 +9,12 @@ internal static class DiagnosticId internal const string ExperimentalFeature = "SENTRY0001"; #endif + //TODO: QUESTION: Should we re-use the above for all [Experimental] features or have one ID per experimental feature? internal const string ExperimentalSentryLogs = "SENTRY0002"; } + +//TODO: not sure about this type name +internal static class UrlFormats +{ + internal const string ExperimentalSentryLogs = "https://github.com/getsentry/sentry-dotnet/issues/4132"; +} diff --git a/src/Sentry/Internal/Extensions/JsonExtensions.cs b/src/Sentry/Internal/Extensions/JsonExtensions.cs index 96b28bf81b..435e9e441f 100644 --- a/src/Sentry/Internal/Extensions/JsonExtensions.cs +++ b/src/Sentry/Internal/Extensions/JsonExtensions.cs @@ -472,6 +472,17 @@ public static void WriteSerializable( writer.WriteSerializableValue(value, logger); } + public static void WriteSerializable( + this Utf8JsonWriter writer, + string propertyName, + TValue value, + IDiagnosticLogger? logger) + where TValue : struct, ISentryJsonSerializable + { + writer.WritePropertyName(propertyName); + value.WriteTo(writer, logger); + } + public static void WriteDynamicValue( this Utf8JsonWriter writer, object? value, diff --git a/src/Sentry/SentryLogger.cs b/src/Sentry/SentryLogger.cs new file mode 100644 index 0000000000..af931f9761 --- /dev/null +++ b/src/Sentry/SentryLogger.cs @@ -0,0 +1,68 @@ +using Sentry.Experimental; +using Sentry.Infrastructure; +using Sentry.Protocol.Envelopes; + +//TODO: add XML docs +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Sentry; + +/// +/// Creates and sends logs to Sentry. +/// +[Experimental(DiagnosticId.ExperimentalSentryLogs, UrlFormat = UrlFormats.ExperimentalSentryLogs)] +public sealed class SentryLogger +{ + //TODO: QUESTION: Trace vs LogTrace + // Trace() is from the Sentry Logs feature specs. LogTrace() would be more .NET idiomatic + public void Trace(string template, object[]? parameters = null, Action? configureLog = null) + { + CaptureLog(SentrySeverity.Trace, template, parameters, configureLog); + } + + //TODO: QUESTION: parameter name "template" vs "format" + // "template" from the "sentry.message.template" attributes of the envelope + // "format" as in System.String.Format to be more idiomatic + public void Debug(string template, object[]? parameters = null, Action? configureLog = null) + { + CaptureLog(SentrySeverity.Debug, template, parameters, configureLog); + } + + public void Info(string template, object[]? parameters = null, Action? configureLog = null) + { + CaptureLog(SentrySeverity.Info, template, parameters, configureLog); + } + + //TODO: QUESTION: Warn vs Warning + // Warn is from the Sentry Logs feature specs. Warning would be more .NET idiomatic + public void Warn(string template, object[]? parameters = null, Action? configureLog = null) + { + CaptureLog(SentrySeverity.Warn, template, parameters, configureLog); + } + + public void Error(string template, object[]? parameters = null, Action? configureLog = null) + { + CaptureLog(SentrySeverity.Error, template, parameters, configureLog); + } + + public void Fatal(string template, object[]? parameters = null, Action? configureLog = null) + { + CaptureLog(SentrySeverity.Fatal, template, parameters, configureLog); + } + + //TODO: consider ReadOnlySpan for TFMs where Span is available + // or: utilize a custom [InterpolatedStringHandler] for modern TFMs + // with which we may not be able to enforce on compile-time to only support string, boolean, integer, double + // but we could have an Analyzer for that, indicating that Sentry does not support other types if used in the interpolated string + // or: utilize a SourceGen, similar to the Microsoft.Extensions.Logging [LoggerMessage] + // with which we could enforce on compile-time to only support string, boolean, integer, double + private static void CaptureLog(SentrySeverity level, string template, object[]? parameters, Action? configureLog) + { + string message = String.Format(template, parameters ?? []); + SentryLog log = new(level, message); + configureLog?.Invoke(log); + + IHub hub = SentrySdk.CurrentHub; + _ = hub.CaptureEnvelope(Envelope.FromLog(log)); + } +} diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 401a0fa6f0..cabb8334a0 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -279,6 +279,15 @@ public void Dispose() /// public static bool IsEnabled { [DebuggerStepThrough] get => CurrentHub.IsEnabled; } + /// + /// Creates and sends logs to Sentry. + /// + //TODO: add to IHub or ISentryClient + // adding to interfaces is breaking, perhaps via a DIM but what about netstandard2.0 runtimes + // or are these interfaces intended to be extended as user code is not meant to implement them + [Experimental(DiagnosticId.ExperimentalSentryLogs, UrlFormat = UrlFormats.ExperimentalSentryLogs)] + public static SentryLogger Logger { get; } = new SentryLogger(); + /// /// Creates a new scope that will terminate when disposed. /// diff --git a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.verify.cs b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.verify.cs index b9f4250999..afc7ed5a5d 100644 --- a/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.verify.cs +++ b/test/Sentry.DiagnosticSource.IntegrationTests/SqlListenerTests.verify.cs @@ -155,7 +155,7 @@ public void ShouldIgnoreAllErrorAndExceptionIds() foreach (var field in eventIds) { var eventId = (EventId)field.GetValue(null)!; - var isEfExceptionMessage = SentryLogger.IsEfExceptionMessage(eventId); + var isEfExceptionMessage = Sentry.Extensions.Logging.SentryLogger.IsEfExceptionMessage(eventId); var name = field.Name; if (name.EndsWith("Exception") || name.EndsWith("Error") || From d96b092acede1f00892c6ca4d3e599d4ddcdf384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Mon, 5 May 2025 14:08:43 +0200 Subject: [PATCH 3/4] style(logs): consolidate --- src/Sentry/Experimental/SentryLog.cs | 24 ---------------------- src/Sentry/Experimental/ValueTypePair.cs | 26 ++++++++++++++++++++++++ src/Sentry/SentryLogger.cs | 4 ++-- 3 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 src/Sentry/Experimental/ValueTypePair.cs diff --git a/src/Sentry/Experimental/SentryLog.cs b/src/Sentry/Experimental/SentryLog.cs index fde5a3106f..f7bdf687ed 100644 --- a/src/Sentry/Experimental/SentryLog.cs +++ b/src/Sentry/Experimental/SentryLog.cs @@ -45,7 +45,6 @@ public required int SeverityNumber get => _severityNumber; set { - // SentrySeverityExtensions.ThrowIfOutOfRange(value); _severityNumber = value; } @@ -130,26 +129,3 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteEndObject(); } } - -//TODO: remove? perhaps a simple System.ValueTuple`2 suffices -internal readonly struct ValueTypePair : ISentryJsonSerializable -{ - public ValueTypePair(object value, string type) - { - Value = value.ToString()!; - Type = type; - } - - public string Value { get; } - public string Type { get; } - - public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) - { - writer.WriteStartObject(); - - writer.WriteString("value", Value); - writer.WriteString("type", Type); - - writer.WriteEndObject(); - } -} diff --git a/src/Sentry/Experimental/ValueTypePair.cs b/src/Sentry/Experimental/ValueTypePair.cs new file mode 100644 index 0000000000..f1d41a9a15 --- /dev/null +++ b/src/Sentry/Experimental/ValueTypePair.cs @@ -0,0 +1,26 @@ +using Sentry.Extensibility; + +namespace Sentry.Experimental; + +//TODO: remove? perhaps a simple System.ValueTuple`2 suffices +internal readonly struct ValueTypePair : ISentryJsonSerializable +{ + public ValueTypePair(object value, string type) + { + Value = value.ToString()!; + Type = type; + } + + public string Value { get; } + public string Type { get; } + + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + + writer.WriteString("value", Value); + writer.WriteString("type", Type); + + writer.WriteEndObject(); + } +} diff --git a/src/Sentry/SentryLogger.cs b/src/Sentry/SentryLogger.cs index af931f9761..9595f06733 100644 --- a/src/Sentry/SentryLogger.cs +++ b/src/Sentry/SentryLogger.cs @@ -58,11 +58,11 @@ public void Fatal(string template, object[]? parameters = null, Action? configureLog) { - string message = String.Format(template, parameters ?? []); + var message = string.Format(template, parameters ?? []); SentryLog log = new(level, message); configureLog?.Invoke(log); - IHub hub = SentrySdk.CurrentHub; + var hub = SentrySdk.CurrentHub; _ = hub.CaptureEnvelope(Envelope.FromLog(log)); } } From 2958a47de2606be8cc11fd8c63e6df559d3ea1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20P=C3=B6lz?= <38893694+Flash0ver@users.noreply.github.com> Date: Mon, 5 May 2025 15:33:54 +0200 Subject: [PATCH 4/4] ref(logs): remove generic WriteSerializable overload --- src/Sentry/Internal/Extensions/JsonExtensions.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Sentry/Internal/Extensions/JsonExtensions.cs b/src/Sentry/Internal/Extensions/JsonExtensions.cs index 435e9e441f..96b28bf81b 100644 --- a/src/Sentry/Internal/Extensions/JsonExtensions.cs +++ b/src/Sentry/Internal/Extensions/JsonExtensions.cs @@ -472,17 +472,6 @@ public static void WriteSerializable( writer.WriteSerializableValue(value, logger); } - public static void WriteSerializable( - this Utf8JsonWriter writer, - string propertyName, - TValue value, - IDiagnosticLogger? logger) - where TValue : struct, ISentryJsonSerializable - { - writer.WritePropertyName(propertyName); - value.WriteTo(writer, logger); - } - public static void WriteDynamicValue( this Utf8JsonWriter writer, object? value,