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,