Skip to content

WIP: add Sentry Logs (experimental) #4308

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Features

- Add experimental support for [Sentry Structured Logging](https://docs.sentry.io/product/explore/logs/) via `SentrySdk.Experimental.Logger` ([#4158](https://github.com/getsentry/sentry-dotnet/pull/4158))
- Added StartSpan and GetTransaction methods to the SentrySdk ([#4303](https://github.com/getsentry/sentry-dotnet/pull/4303))

### Fixes
Expand Down
20 changes: 20 additions & 0 deletions samples/Sentry.Samples.Console.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* - Error Monitoring (both handled and unhandled exceptions)
* - Performance Tracing (Transactions / Spans)
* - Release Health (Sessions)
* - Logs
* - MSBuild integration for Source Context (see the csproj)
*
* For more advanced features of the SDK, see Sentry.Samples.Console.Customized.
Expand Down Expand Up @@ -35,6 +36,20 @@

// This option tells Sentry to capture 100% of traces. You still need to start transactions and spans.
options.TracesSampleRate = 1.0;

// This option enables Sentry Logs created via SentrySdk.Logger.
options.Experimental.EnableLogs = true;
options.Experimental.SetBeforeSendLog(static log =>
{
// A demonstration of how you can drop logs based on some attribute they have
if (log.TryGetAttribute("suppress", out var attribute) && attribute is true)
{
return null;
}

// Drop logs with level Info
return log.Level is SentryLogLevel.Info ? null : log;
});
});

// This starts a new transaction and attaches it to the scope.
Expand All @@ -58,6 +73,7 @@ async Task FirstFunction()
var httpClient = new HttpClient(messageHandler, true);
var html = await httpClient.GetStringAsync("https://example.com/");
WriteLine(html);
SentrySdk.Experimental.Logger.LogInfo("HTTP Request completed.");
}

async Task SecondFunction()
Expand All @@ -77,6 +93,8 @@ async Task SecondFunction()
// This is an example of capturing a handled exception.
SentrySdk.CaptureException(exception);
span.Finish(exception);

SentrySdk.Experimental.Logger.LogError("Error with message: {0}", [exception.Message], static log => log.SetAttribute("method", nameof(SecondFunction)));
}

span.Finish();
Expand All @@ -90,6 +108,8 @@ async Task ThirdFunction()
// Simulate doing some work
await Task.Delay(100);

SentrySdk.Experimental.Logger.LogFatal("Crash imminent!", [], static log => log.SetAttribute("suppress", true));

// This is an example of an unhandled exception. It will be captured automatically.
throw new InvalidOperationException("Something happened that crashed the app!");
}
Expand Down
11 changes: 11 additions & 0 deletions src/Sentry/BindableSentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ internal partial class BindableSentryOptions
public bool? EnableSpotlight { get; set; }
public string? SpotlightUrl { get; set; }

[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
public BindableSentryExperimentalOptions Experimental { get; set; } = new();

[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
internal sealed class BindableSentryExperimentalOptions
{
public bool? EnableLogs { get; set; }
}

public void ApplyTo(SentryOptions options)
{
options.IsGlobalModeEnabled = IsGlobalModeEnabled ?? options.IsGlobalModeEnabled;
Expand Down Expand Up @@ -100,6 +109,8 @@ public void ApplyTo(SentryOptions options)
options.EnableSpotlight = EnableSpotlight ?? options.EnableSpotlight;
options.SpotlightUrl = SpotlightUrl ?? options.SpotlightUrl;

options.Experimental.EnableLogs = Experimental.EnableLogs ?? options.Experimental.EnableLogs;

#if ANDROID
Android.ApplyTo(options.Android);
Native.ApplyTo(options.Native);
Expand Down
22 changes: 22 additions & 0 deletions src/Sentry/Extensibility/DiagnosticLoggerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ internal static void LogDebug<TArg, TArg2>(
TArg2 arg2)
=> options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Debug, null, message, arg, arg2);

/// <summary>
/// Log a debug message.
/// </summary>
public static void LogDebug<TArg, TArg2, TArg3>(
this IDiagnosticLogger logger,
string message,
TArg arg,
TArg2 arg2,
TArg3 arg3)
=> logger.LogIfEnabled(SentryLevel.Debug, null, message, arg, arg2, arg3);

/// <summary>
/// Log a debug message.
/// </summary>
Expand Down Expand Up @@ -233,6 +244,17 @@ internal static void LogWarning<TArg, TArg2>(
TArg2 arg2)
=> options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Warning, null, message, arg, arg2);

/// <summary>
/// Log a warning message.
/// </summary>
public static void LogWarning<TArg, TArg2, TArg3>(
this IDiagnosticLogger logger,
string message,
TArg arg,
TArg2 arg2,
TArg3 arg3)
=> logger.LogIfEnabled(SentryLevel.Warning, null, message, arg, arg2, arg3);

/// <summary>
/// Log a warning message.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,11 @@ public void CaptureUserFeedback(UserFeedback userFeedback)
/// No-Op.
/// </summary>
public SentryId LastEventId => SentryId.Empty;

/// <summary>
/// Disabled Logger.
/// <para>This API is experimental and it may change in the future.</para>
/// </summary>
[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger => DisabledSentryStructuredLogger.Instance;
}
7 changes: 7 additions & 0 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ private HubAdapter() { }
/// </summary>
public SentryId LastEventId { [DebuggerStepThrough] get => SentrySdk.LastEventId; }

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// <para>This API is experimental and it may change in the future.</para>
/// </summary>
[Experimental(DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger { [DebuggerStepThrough] get => SentrySdk.Experimental.Logger; }

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/HubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,16 @@ internal static ITransactionTracer StartTransaction(
var transaction = hub.GetTransaction();
return transaction?.IsSampled == true ? transaction : null;
}

internal static Scope? GetScope(this IHub hub)
{
if (hub is Hub fullHub)
{
return fullHub.ScopeManager.GetCurrent().Key;
}

Scope? current = null;
hub.ConfigureScope(scope => current = scope);
return current;
}
}
14 changes: 14 additions & 0 deletions src/Sentry/IHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ public interface IHub : ISentryClient, ISentryScopeManager
/// </summary>
public SentryId LastEventId { get; }

/// <summary>
/// Creates and sends logs to Sentry.
/// <para>This API is experimental and it may change in the future.</para>
/// </summary>
/// <remarks>
/// Available options:
/// <list type="bullet">
/// <item><see cref="Sentry.SentryOptions.SentryExperimentalOptions.EnableLogs"/></item>
/// <item><see cref="Sentry.SentryOptions.SentryExperimentalOptions.SetBeforeSendLog(System.Func{SentryLog, SentryLog})"/></item>
/// </list>
/// </remarks>
[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger { get; }

/// <summary>
/// Starts a transaction.
/// </summary>
Expand Down
2 changes: 0 additions & 2 deletions src/Sentry/Infrastructure/DiagnosticId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ namespace Sentry.Infrastructure;

internal static class DiagnosticId
{
#if NET5_0_OR_GREATER
/// <summary>
/// Indicates that the feature is experimental and may be subject to change or removal in future versions.
/// </summary>
internal const string ExperimentalFeature = "SENTRY0001";
#endif
}
79 changes: 79 additions & 0 deletions src/Sentry/Internal/DefaultSentryStructuredLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Sentry.Extensibility;
using Sentry.Infrastructure;
using Sentry.Protocol.Envelopes;

namespace Sentry.Internal;

internal sealed class DefaultSentryStructuredLogger : SentryStructuredLogger
{
private readonly IHub _hub;
private readonly SentryOptions _options;
private readonly ISystemClock _clock;

internal DefaultSentryStructuredLogger(IHub hub, SentryOptions options, ISystemClock clock)
{
Debug.Assert(options is { Experimental.EnableLogs: true });

_hub = hub;
_options = options;
_clock = clock;
}

private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog)
{
var timestamp = _clock.GetUtcNow();
var traceHeader = _hub.GetTraceHeader() ?? SentryTraceHeader.Empty;

string message;
try
{
message = string.Format(CultureInfo.InvariantCulture, template, parameters ?? []);
}
catch (FormatException e)
{
_options.DiagnosticLogger?.LogError(e, "Template string does not match the provided argument. The Log will be dropped.");
return;
}

SentryLog log = new(timestamp, traceHeader.TraceId, level, message)
{
Template = template,
Parameters = ImmutableArray.Create(parameters),
ParentSpanId = traceHeader.SpanId,
};

try
{
configureLog?.Invoke(log);
}
catch (Exception e)
{
_options.DiagnosticLogger?.LogError(e, "The configureLog callback threw an exception. The Log will be dropped.");
return;
}

var scope = _hub.GetScope();
log.SetDefaultAttributes(_options, scope?.Sdk ?? SdkVersion.Instance);

var configuredLog = log;
if (_options.Experimental.BeforeSendLogInternal is { } beforeSendLog)
{
try
{
configuredLog = beforeSendLog.Invoke(log);
}
catch (Exception e)
{
_options.DiagnosticLogger?.LogError(e, "The BeforeSendLog callback threw an exception. The Log will be dropped.");
return;
}
}

if (configuredLog is not null)
{
//TODO: enqueue in Batch-Processor / Background-Worker
// see https://github.com/getsentry/sentry-dotnet/issues/4132
_ = _hub.CaptureEnvelope(Envelope.FromLog(configuredLog));
}
}
}
15 changes: 15 additions & 0 deletions src/Sentry/Internal/DisabledSentryStructuredLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Sentry.Internal;

internal sealed class DisabledSentryStructuredLogger : SentryStructuredLogger
{
internal static DisabledSentryStructuredLogger Instance { get; } = new DisabledSentryStructuredLogger();

internal DisabledSentryStructuredLogger()
{
}

private protected override void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog)
{
// disabled
}
}
5 changes: 5 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ internal Hub(
PushScope();
}

Logger = SentryStructuredLogger.Create(this, options, _clock);

#if MEMORY_DUMP_SUPPORTED
if (options.HeapDumpOptions is not null)
{
Expand Down Expand Up @@ -818,4 +820,7 @@ public void Dispose()
}

public SentryId LastEventId => CurrentScope.LastEventId;

[Experimental(DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger { get; }
}
16 changes: 16 additions & 0 deletions src/Sentry/Protocol/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,22 @@ internal static Envelope FromClientReport(ClientReport clientReport)
return new Envelope(header, items);
}

// TODO: This is temporary. We don't expect single log messages to become an envelope by themselves since batching is needed
[Experimental(DiagnosticId.ExperimentalFeature)]
internal static Envelope FromLog(SentryLog log)
{
//TODO: allow batching Sentry logs
//see https://github.com/getsentry/sentry-dotnet/issues/4132
var header = DefaultHeader;

var items = new[]
{
EnvelopeItem.FromLog(log)
};

return new Envelope(header, items);
}

private static async Task<IReadOnlyDictionary<string, object?>> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
Expand Down
16 changes: 16 additions & 0 deletions src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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";
Expand Down Expand Up @@ -370,6 +371,21 @@ internal static EnvelopeItem FromClientReport(ClientReport report)
return new EnvelopeItem(header, new JsonSerializable(report));
}

[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
internal static EnvelopeItem FromLog(SentryLog log)
{
//TODO: allow batching Sentry logs
//see https://github.com/getsentry/sentry-dotnet/issues/4132
var header = new Dictionary<string, object?>(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<Dictionary<string, object?>> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
Expand Down
Loading
Loading