Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Reduced memory pressure when sampling less than 100% of traces/transactions ([#4212](https://github.com/getsentry/sentry-dotnet/pull/4212))

### Fixes

- Support musl on Linux ([#4188](https://github.com/getsentry/sentry-dotnet/pull/4188))
Expand Down
12 changes: 8 additions & 4 deletions src/Sentry.AspNetCore/SentryTracingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,11 @@ public async Task InvokeAsync(HttpContext context)
}
finally
{
if (transaction is not null)
if (transaction is UnsampledTransaction)
{
transaction.Finish();
}
else if (transaction is TransactionTracer tracer)
{
// The Transaction name was altered during the pipeline execution,
// That could be done by user interference or by some Event Capture
Expand Down Expand Up @@ -183,15 +187,15 @@ public async Task InvokeAsync(HttpContext context)
if (!string.IsNullOrEmpty(customTransactionName))
{
transaction.Name = $"{method} {customTransactionName}";
((TransactionTracer)transaction).NameSource = TransactionNameSource.Custom;
tracer.NameSource = TransactionNameSource.Custom;
}
else
{
// Finally, fallback to using the URL path.
// e.g. "GET /pets/1"
var path = context.Request.Path;
transaction.Name = $"{method} {path}";
((TransactionTracer)transaction).NameSource = TransactionNameSource.Url;
tracer.NameSource = TransactionNameSource.Url;
}
}

Expand All @@ -200,7 +204,7 @@ public async Task InvokeAsync(HttpContext context)
transaction.Finish(status);
}
// Status code not yet changed to 500 but an exception does exist
// so lets avoid passing the misleading 200 down and close only with
// so let's avoid passing the misleading 200 down and close only with
// the exception instance that will be inferred as errored.
else if (status == SpanStatus.Ok)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private async Task<TransactionContext> StartOrContinueTraceAsync(FunctionContext
if (requestData is null)
{
// not an HTTP trigger
return SentrySdk.ContinueTrace((SentryTraceHeader?)null, (BaggageHeader?)null, transactionName, Operation);
return _hub.ContinueTrace((SentryTraceHeader?)null, (BaggageHeader?)null, transactionName, Operation);
}

var httpMethod = requestData.Method.ToUpperInvariant();
Expand Down
28 changes: 28 additions & 0 deletions src/Sentry/DynamicSamplingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,31 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
replaySession);
}

public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession)
{
// These should already be set on the transaction.
var publicKey = options.ParsedDsn.PublicKey;
var traceId = transaction.TraceId;
var sampled = transaction.IsSampled;
var sampleRate = transaction.SampleRate!.Value;
var sampleRand = transaction.SampleRand;
var transactionName = transaction.NameSource.IsHighQuality() ? transaction.Name : null;

// These two may not have been set yet on the transaction, but we can get them directly.
var release = options.SettingLocator.GetRelease();
var environment = options.SettingLocator.GetEnvironment();

return new DynamicSamplingContext(traceId,
publicKey,
sampled,
sampleRate,
sampleRand,
release,
environment,
transactionName,
replaySession);
}

public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
{
var traceId = propagationContext.TraceId;
Expand All @@ -224,6 +249,9 @@ internal static class DynamicSamplingContextExtensions
public static DynamicSamplingContext CreateDynamicSamplingContext(this TransactionTracer transaction, SentryOptions options, IReplaySession? replaySession)
=> DynamicSamplingContext.CreateFromTransaction(transaction, options, replaySession);

public static DynamicSamplingContext CreateDynamicSamplingContext(this UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession)
=> DynamicSamplingContext.CreateFromUnsampledTransaction(transaction, options, replaySession);

public static DynamicSamplingContext CreateDynamicSamplingContext(this SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
=> DynamicSamplingContext.CreateFromPropagationContext(propagationContext, options, replaySession);
}
101 changes: 56 additions & 45 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,61 +129,72 @@ internal ITransactionTracer StartTransaction(
IReadOnlyDictionary<string, object?> customSamplingContext,
DynamicSamplingContext? dynamicSamplingContext)
{
var transaction = new TransactionTracer(this, context)
{
SampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var sampleRand) ?? false
? double.Parse(sampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
: SampleRandHelper.GenerateSampleRand(context.TraceId.ToString())
};

// If the hub is disabled, we will always sample out. In other words, starting a transaction
// after disposing the hub will result in that transaction not being sent to Sentry.
// Additionally, we will always sample out if tracing is explicitly disabled.
// Do not invoke the TracesSampler, evaluate the TracesSampleRate, and override any sampling decision
// that may have been already set (i.e.: from a sentry-trace header).
if (!IsEnabled)
{
transaction.IsSampled = false;
transaction.SampleRate = 0.0;
return NoOpTransaction.Instance;
}
else
{
// Except when tracing is disabled, TracesSampler runs regardless of whether a decision
// has already been made, as it can be used to override it.
if (_options.TracesSampler is { } tracesSampler)
{
var samplingContext = new TransactionSamplingContext(
context,
customSamplingContext);

if (tracesSampler(samplingContext) is { } sampleRate)
{
transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate);
transaction.SampleRate = sampleRate;
}
}
bool? isSampled = null;
double? sampleRate = null;
var sampleRand = dynamicSamplingContext?.Items.TryGetValue("sample_rand", out var dscsampleRand) ?? false
? double.Parse(dscsampleRand, NumberStyles.Float, CultureInfo.InvariantCulture)
: SampleRandHelper.GenerateSampleRand(context.TraceId.ToString());

// Random sampling runs only if the sampling decision hasn't been made already.
if (transaction.IsSampled == null)
// TracesSampler runs regardless of whether a decision has already been made, as it can be used to override it.
if (_options.TracesSampler is { } tracesSampler)
{
var samplingContext = new TransactionSamplingContext(
context,
customSamplingContext);

if (tracesSampler(samplingContext) is { } samplerSampleRate)
{
var sampleRate = _options.TracesSampleRate ?? 0.0;
transaction.IsSampled = SampleRandHelper.IsSampled(transaction.SampleRand.Value, sampleRate);
transaction.SampleRate = sampleRate;
// The TracesSampler trumps all other sampling decisions (even the trace header)
sampleRate = samplerSampleRate;
isSampled = SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);
}
}

if (transaction.IsSampled is true &&
_options.TransactionProfilerFactory is { } profilerFactory &&
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
// If the sampling decision isn't made by a trace sampler we check the trace header first (from the context) or
// finally fallback to Random sampling if the decision has been made by no other means
sampleRate ??= _options.TracesSampleRate ?? 0.0;
isSampled ??= context.IsSampled ?? SampleRandHelper.IsSampled(sampleRand, sampleRate.Value);

// Make sure there is a replayId (if available) on the provided DSC (if any).
dynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession);

if (isSampled is false)
{
var unsampledTransaction = new UnsampledTransaction(this, context)
{
// TODO cancellation token based on Hub being closed?
transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None);
}
SampleRate = sampleRate,
SampleRand = sampleRand,
DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
};
// If no DSC was provided, create one based on this transaction.
// Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions).
unsampledTransaction.DynamicSamplingContext ??= unsampledTransaction.CreateDynamicSamplingContext(_options, _replaySession);
return unsampledTransaction;
}

// Use the provided DSC (adding the active replayId if necessary), or create one based on this transaction.
// DSC creation must be done AFTER the sampling decision has been made.
transaction.DynamicSamplingContext = dynamicSamplingContext?.WithReplayId(_replaySession)
?? transaction.CreateDynamicSamplingContext(_options, _replaySession);
var transaction = new TransactionTracer(this, context)
{
SampleRate = sampleRate,
SampleRand = sampleRand,
DynamicSamplingContext = dynamicSamplingContext // Default to the provided DSC
};
// If no DSC was provided, create one based on this transaction.
// Must be done AFTER the sampling decision has been made (the DSC propagates sampling decisions).
transaction.DynamicSamplingContext ??= transaction.CreateDynamicSamplingContext(_options, _replaySession);

if (_options.TransactionProfilerFactory is { } profilerFactory &&
_randomValuesFactory.NextBool(_options.ProfilesSampleRate ?? 0.0))
{
// TODO cancellation token based on Hub being closed?
transaction.TransactionProfiler = profilerFactory.Start(transaction, CancellationToken.None);
}

// A sampled out transaction still appears fully functional to the user
// but will be dropped by the client and won't reach Sentry's servers.
Expand Down Expand Up @@ -220,7 +231,7 @@ public SentryTraceHeader GetTraceHeader()
public BaggageHeader GetBaggage()
{
var span = GetSpan();
if (span?.GetTransaction() is TransactionTracer { DynamicSamplingContext: { IsEmpty: false } dsc })
if (span?.GetTransaction().GetDynamicSamplingContext() is { IsEmpty: false } dsc)
{
return dsc.ToBaggageHeader();
}
Expand Down Expand Up @@ -373,9 +384,9 @@ private void ApplyTraceContextToEvent(SentryEvent evt, ISpan span)
evt.Contexts.Trace.TraceId = span.TraceId;
evt.Contexts.Trace.ParentSpanId = span.ParentSpanId;

if (span.GetTransaction() is TransactionTracer transactionTracer)
if (span.GetTransaction().GetDynamicSamplingContext() is { } dsc)
{
evt.DynamicSamplingContext = transactionTracer.DynamicSamplingContext;
evt.DynamicSamplingContext = dsc;
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/Sentry/Internal/ITransactionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Sentry.Internal;

internal static class TransactionExtensions
{
public static DynamicSamplingContext? GetDynamicSamplingContext(this ITransactionTracer transaction)
{
if (transaction is UnsampledTransaction unsampledTransaction)
{
return unsampledTransaction.DynamicSamplingContext;
}
if (transaction is TransactionTracer transactionTracer)
{
return transactionTracer.DynamicSamplingContext;
}
return null;
}
}
24 changes: 12 additions & 12 deletions src/Sentry/Internal/NoOpSpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ protected NoOpSpan()
{
}

public SpanId SpanId => SpanId.Empty;
public virtual SpanId SpanId => SpanId.Empty;
public SpanId? ParentSpanId => SpanId.Empty;
public SentryId TraceId => SentryId.Empty;
public bool? IsSampled => default;
public virtual SentryId TraceId => SentryId.Empty;
public virtual bool? IsSampled => default;
public IReadOnlyDictionary<string, string> Tags => ImmutableDictionary<string, string>.Empty;
public IReadOnlyDictionary<string, object?> Extra => ImmutableDictionary<string, object?>.Empty;
public IReadOnlyDictionary<string, object?> Data => ImmutableDictionary<string, object?>.Empty;
public DateTimeOffset StartTimestamp => default;
public DateTimeOffset? EndTimestamp => default;
public bool IsFinished => default;
public DateTimeOffset? EndTimestamp => null;
public virtual bool IsFinished => false;

public string Operation
public virtual string Operation
{
get => string.Empty;
set { }
Expand All @@ -42,21 +42,21 @@ public SpanStatus? Status
set { }
}

public ISpan StartChild(string operation) => this;
public virtual ISpan StartChild(string operation) => this;

public void Finish()
public virtual void Finish()
{
}

public void Finish(SpanStatus status)
public virtual void Finish(SpanStatus status)
{
}

public void Finish(Exception exception, SpanStatus status)
public virtual void Finish(Exception exception, SpanStatus status)
{
}

public void Finish(Exception exception)
public virtual void Finish(Exception exception)
{
}

Expand All @@ -76,7 +76,7 @@ public void SetData(string key, object? value)
{
}

public SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty;
public virtual SentryTraceHeader GetTraceHeader() => SentryTraceHeader.Empty;

public IReadOnlyDictionary<string, Measurement> Measurements => ImmutableDictionary<string, Measurement>.Empty;

Expand Down
6 changes: 3 additions & 3 deletions src/Sentry/Internal/NoOpTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ internal class NoOpTransaction : NoOpSpan, ITransactionTracer
{
public new static ITransactionTracer Instance { get; } = new NoOpTransaction();

private NoOpTransaction()
protected NoOpTransaction()
{
}

public SdkVersion Sdk => SdkVersion.Instance;

public string Name
public virtual string Name
{
get => string.Empty;
set { }
Expand Down Expand Up @@ -87,7 +87,7 @@ public IReadOnlyList<string> Fingerprint
set { }
}

public IReadOnlyCollection<ISpan> Spans => ImmutableList<ISpan>.Empty;
public virtual IReadOnlyCollection<ISpan> Spans => ImmutableList<ISpan>.Empty;

public IReadOnlyCollection<Breadcrumb> Breadcrumbs => ImmutableList<Breadcrumb>.Empty;

Expand Down
Loading
Loading