getsentry / sentry-dotnet

Sentry SDK for .NET
https://docs.sentry.io/platforms/dotnet
MIT License
582 stars 207 forks source link

Improve guidance for .NET Worker Services #2274

Open mattjohnsonpint opened 1 year ago

mattjohnsonpint commented 1 year ago

Problem Statement

We should provide documentation and samples for integrating Sentry with .NET Worker Services.

Note, we already have the Generic Host sample, but this is slightly different.

Solution Brainstorm

mattjohnsonpint commented 1 year ago

Also, explain clearly that global mode should be false for background workers. Update docs for IsGlobalMode.

See https://twitter.com/vekzdran/status/1646886760096362497

mattjohnsonpint commented 1 year ago

POC:

public class Worker : BackgroundService
{
    private readonly IHub _hub;

    public Worker(IHub hub)
    {
        _hub = hub;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var _ = _hub.PushScope();
            var transaction = _hub.StartTransaction($"{nameof(Worker)}.{nameof(DoWorkAsync)}", "function");
            try
            {
                _hub.ConfigureScope(scope =>
                {
                    scope.Clear();
                    scope.Transaction = transaction;
                });

                await DoWorkAsync(stoppingToken);
            }
            catch(Exception exception)
            {
                transaction.Finish(exception);
            }
            finally
            {
                transaction.Finish();
            }
        }
    }
}
mattjohnsonpint commented 1 year ago

Updated POC. Requires vNext (3.31.0) for #2303 & #2309.

public class Worker : BackgroundService
{
    private readonly IHub _hub;

    public Worker(IHub hub)
    {
        _hub = hub;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _hub.WithScopeAsync(async scope =>
            {
                scope.Clear();

                var transaction = _hub.StartTransaction($"{nameof(Worker)}.{nameof(DoWorkAsync)}", "function");
                scope.Transaction = transaction;

                try
                {
                    await DoWorkAsync(stoppingToken);
                }
                catch (Exception exception)
                {
                    transaction.Finish(exception);
                }
                finally
                {
                    transaction.Finish();
                }
            });
        }
    }
}
Kampfmoehre commented 8 months ago

@mattjohnsonpint Just a quick follow up question, how would I retrieve a transaction like the one started in your sample? With SentrySdk.GetSpan()?

ISpan? span = SentrySdk.GetSpan();
if (span is ITransaction transaction)
{
  transaction.StartChild("Something");
}
mattjohnsonpint commented 8 months ago

@bruno-garcia

jamescrosswell commented 8 months ago

@mattjohnsonpint Just a quick follow up question, how would I retrieve a transaction like the one started in your sample? With SentrySdk.GetSpan()?

ISpan? span = SentrySdk.GetSpan();
if (span is ITransaction transaction)
{
  transaction.StartChild("Something");
}

@Kampfmoehre could you give a bit more context? Are you trying to get the currently active transaction or a specific transaction (one that you've seen at some point earlier in your code)?

Kampfmoehre commented 8 months ago

@jamescrosswell I have a worker service that is calling a method which is also used by others. I don't want to pass the transaction directly to it, since all other callers then also would need to provide one and it would make unit tests more complex.

The transaction is either the one I have manually started in the worker loop (or from another worker that also calls this method) or it could be one that was set by Sentry itself from a HTTP request somewhere else in the code.

Let's just say the DoWorkAsync method wants to provide some spans to a transaction if one exists.

Should it look like this or are there better ways to achieve this?

public async Task DoWorkAsync(CancellationToken cancellationToken)
{
  ISpan? span = SentrySdk.GetSpan();
  ISpan? loadDataSpan = span?.StarChild("load-data");
  var someData = await repo.LoadSomethingAsync();
  loadDataSpan?.Finish(SpanStatus.Ok);

  ISpan? processDataSpan = span?.StartChild("process-data");
  await repo.ProcessData(someData);
  processDataSpan?.Finish(SpanStatus.Ok);

  // ...
}
jamescrosswell commented 8 months ago

Should it look like this or are there better ways to achieve this?

That looks fine in that case. Just be aware that when Sentry isn't initialised (e.g. when the code is run by unit tests) that SentrySdk.GetSpan() will return null since the underlying implementation being called in that instance is the DisabledHub.

Also, much more importantly, none of the code in calls like this will get executed (for the same reason):

SentrySdk.ConfigureScope(scope => {
    // Any code you write here only gets executed when Sentry is initiailised
});
MichaelLHerman commented 1 month ago

@mattjohnsonpint scope.Clear and .WithScope don't exist anymore, so the samples above no longer work.

I was looking at this issue for means to introduce IsGlobalScopeEnabled = false on MAUI mobile client since my app has multiple transactions happening at once and spans get recorded to the wrong transaction.

mattjohnsonpint commented 1 month ago

@MichaelLHerman - I don't work for Sentry (or on this project) any more, but I'm sure @jamescrosswell will be happy to help you.

jamescrosswell commented 1 month ago

Hi @MichaelLHerman,

scope.Clear and .WithScope don't exist anymore, so the samples above no longer work.

Generally you can replace WithScope with overloads of the Capture* methods accepting a scope callback. In the case of the POC above, I see CaptureTransaction isn't called explicitly so Matt's first/original POC would be more appropriate.

I was looking at this issue for means to introduce IsGlobalScopeEnabled = false on MAUI mobile client since my app has multiple transactions happening at once and spans get recorded to the wrong transaction.

I see you opened a new issue for this - thank you for doing that and for the extra context! I'll reply there: