Skip to content

feat(logs): initial API for Sentry Logs #4158

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

Open
wants to merge 50 commits into
base: feat/logs
Choose a base branch
from

Conversation

Flash0ver
Copy link
Member

@Flash0ver Flash0ver commented Apr 30, 2025

Draft PR: Work in progress

Info

TODO

  • Complete public API surface area
  • XML API docs
  • Add Tests

Follow-ups

  • Add non object[] but generic <TArg> overloads per Log-Severity method for the most common scenarios
    • to avoid both boxing allocations and array allocations in case options.EnableLogs is false
    • our public IDiagnosticLogger.Log{Level}() extension methods have overloads with up to 3 generic type parameters
    • .NET's String.Format has 1-3 object arguments
      • and 1-3 generic arguments since net8.0 when used with CompositeFormat
  • Consider a custom [InterpolatedStringHandler] to support interpolated strings and ensure zero-alloc if options.EnableLogs is false
    • available for TFMs compatible with net6.0
  • Batching, via log Envelope Item Payload
  • Integrate into Sentry.Extensions.Logging
  • Experimental docs

Early feedback appreciated on

  • public APIs

see also Question-Comments in the draft

@Flash0ver Flash0ver self-assigned this Apr 30, 2025
namespace Sentry.Experimental;

//TODO: QUESTION: not sure about the name
// this is a bit different to Sentry.SentryLevel and Sentry.BreadcrumbLevel
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be different? I wonder if we can use SentryLevel instead?
Because we don't have Trace there? :~

We could add it as -1 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, not necessarily.
And adding it as -1 would not break any existing behavior - but may warrant some documentation - like:

public static LogLevel ToMicrosoft(this SentryLevel level)


But reusing Sentry.SentryLevel could be confusing:

public enum SentryLevel : short

uses the EnumMemberAttribute, with e.g. [EnumMember(Value = "warning")] Warning,. But the OTEL SeverityText field and our Protocol expects "warn" instead. Well, we would still have the separate ToSentryProtocolLevel (or similar) extension method that does the right thing, but the re-use may cause confusion.


Also, this would have a side effect onto existing usages, viaSentryOptions.DiagnosticLevel:

public abstract class DiagnosticLogger : IDiagnosticLogger

Do we want to extend the "Log-Level" for existing internal/diagnostic logging?


On the other hand, adding yet another "Log-Level" enum, that is similar, but not quite the same, is similarly confusing.


And now I am thinking if we could get away with making this new enum type internal instead, since we do have separate Log{Level}() methods, and application code could still change the severity via the int-based SeverityNumber through the BeforeSendLog-callback. (see Log Severity Number)
To, for now, reduce the public surface area, and we could think about which approach to chose when/if the issue arises later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently still have a new enum.
Renamed to SentryLogLevel. Matches the sentry-java SDK.
Also moved to Sentry namespace.

@bruno-garcia
Copy link
Member

Exciting first step in our logging story :)

@jamescrosswell
Copy link
Collaborator

@Flash0ver can we reference some developer docs and/or an issue in the PR description to help reviewers get an overview of the change before looking at code?

@jamescrosswell
Copy link
Collaborator

Looking good so far 👍🏻


private void CaptureLog(SentryLogLevel level, string template, object[]? parameters, Action<SentryLog>? configureLog)
{
Debug.Assert(_options is not null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not IsEnabled check here? and by pass all the public method checks

Copy link
Member Author

@Flash0ver Flash0ver May 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to avoid the CaptureLog method invocation from the public Log{Level} methods if logging is not enabled.
This assert is to let null-flow analysis know that the options are not null here, because we already checked in the calling method.

Changed to Debug.Assert(IsEnabled); to be more explicit about the expected flow.

@Flash0ver Flash0ver changed the title WIP: Sentry Logs feat(logs): initial API for Sentry Logs May 15, 2025
@Flash0ver Flash0ver marked this pull request as ready for review May 15, 2025 18:55
@Flash0ver Flash0ver requested a review from aritchie as a code owner May 15, 2025 18:55
Copy link
Contributor

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 9a51033

@jamescrosswell jamescrosswell self-requested a review May 15, 2025 22:48
Copy link
Collaborator

@jamescrosswell jamescrosswell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great.

The main suggestion I've left is about testability... which I think you'd bump into if you tried to create some unit tests for the new methods that have been added to theHub class.

@@ -33,6 +33,19 @@

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

// This option enables the (experimental) Sentry Logs.
options.EnableLogs = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If/while these are experimental, should we put them in an options.Experimental.EnableLogs section of the options (to make that unavoidably apparent to SDK users)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess because they have [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)] it could be OK without it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's almost worth writing a source generator to maintain this file... so much boilerplate. Not for this PR obviously.

[Experimental(DiagnosticId.ExperimentalFeature)]
public void LogTrace(string template, object[]? parameters = null, Action<SentryLog>? configureLog = null)
{
if (IsEnabled)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like all of the public methods have this IsEnabled check and then delegate all the work to the CaptureLog method.

We could centralise this logic by putting the check for IsEnabled at the top of the CaptureLog method instead.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, if we did add an ISentryStructuredLogger interface, we could have two implementations of that interface: SentryStructuredLogger and DisabledSentryStructuredLogger... the implementation for the later would just be a bunch of no-ops.

One advantage of that approach is that we could mark the dependencies for this implementation as non nullable. I'm not sure if that would simplify things enough to be worth while though (it's a pretty simple class in the first place).

Your call I think.

/// </list>
/// </remarks>
[Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)]
public SentryStructuredLogger Logger { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking about testing of the Hub classes in general, the Logger is kind of a dependency of the hub and currently it's not very easy to inject/mock.

Maybe worth creating an ISentryStructuredLogger interface and having this member be of type ISentryStructuredLogger rather than SentryStructuredLogger. A mock Logger could then optionally be injected via the constructor for test purposes.

namespace Sentry;

/// <summary>
/// Represents the Sentry Log protocol.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason this isn't in the Sentry.Protocol namespace?

/// <remarks>
/// Sent as seconds since the Unix epoch.
/// </remarks>
[Experimental(DiagnosticId.ExperimentalFeature)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly overkill decorating every property with this... the class is already decorated with Experimental.

I think it's enough if we put the option to enable this feature under SentryOptions.Experimenta and then decorate any public methods in the API (the stuff on IHub I'm guessing).

Potentially we could decorate public classes like this as well, although I can't see anybody using these classes independently of the experimental APIs they support.

/// The severity of the structured log.
/// <para>This API is experimental and it may change in the future.</para>
/// </summary>
/// <seealso href="https://develop.sentry.dev/sdk/telemetry/logs/"/>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth adding something to the comments here about the significance of the int values assigned to each of the enum members. I was initially a bit surprised we were assigning values here... only worked out why by reading these docs:


reader.EndOfStream.Should().BeTrue();

header.ToIndentedJsonString().Should().Be($$"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the sort of thing Verify is good at testing... might be a good excuse to get familiar with that.

Copy link
Member

@bruno-garcia bruno-garcia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just went half the PR this time but leaving some notes for now

@@ -33,6 +33,19 @@

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

// This option enables the (experimental) Sentry Logs.
options.EnableLogs = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess because they have [Experimental(Infrastructure.DiagnosticId.ExperimentalFeature)] it could be OK without it?

options.EnableLogs = true;
options.SetBeforeSendLog(static log =>
{
if (log.TryGetAttribute("suppress", out bool attribute) && attribute)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (log.TryGetAttribute("suppress", out bool attribute) && attribute)
// A demonstration of how you can drop logs based on some attribute they have
if (log.TryGetAttribute("suppress", out bool attribute) && attribute)

@@ -88,6 +104,8 @@ async Task ThirdFunction()
// Simulate doing some work
await Task.Delay(100);

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Odd that we must pass an empty array as a second argument.
There's no way we can set up the overloads in a way that this is not needed?

{
internal static void WriteAttribute(Utf8JsonWriter writer, string propertyName, SentryAttribute attribute)
{
Debug.Assert(attribute.Type is not null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this assert on all calls?
it's odd to have it here but no on the others
Or only on the method these all fall into: WriteAttributeValue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants