Skip to content
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
27 changes: 27 additions & 0 deletions src/Sentry.DiagnosticSource/Internal/Tracing/ActivityShim.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Alias required because Android TFMs of the core Sentry package otherwise see an ambiguous reference
// between System.Diagnostics.Activity (global using) and Android.App.Activity (Android implicit usings).
using Activity = System.Diagnostics.Activity;

namespace Sentry.Internal.Tracing;

/// <summary>
/// The ActivitySource used for Activities created by the Sentry tracing shim (i.e. when users call
/// SentrySdk.StartTransaction / ISpan.StartChild and Activity-based tracing is enabled).
/// </summary>
internal static class SentryActivitySources
{
public const string ShimSourceName = "Sentry";

public static readonly ActivitySource Shim = new(ShimSourceName);
}

/// <summary>
/// Keys for side-channel values fused onto an <see cref="Activity"/> by the shim, read back by
/// <see cref="SentryActivityProcessor"/>. These carry Sentry concepts that have no native representation
/// on an Activity (rich span status, custom sampling context, an inbound dynamic sampling context).
/// </summary>
internal static class ShimKeys
{
public const string SpanStatus = "sentry.shim.status";
public const string CustomSamplingContext = "sentry.shim.custom_sampling_context";
}
136 changes: 136 additions & 0 deletions src/Sentry.DiagnosticSource/Internal/Tracing/ActivitySpanShim.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using Sentry.Protocol;

// Alias required because Android TFMs of the core Sentry package otherwise see an ambiguous reference
// between System.Diagnostics.Activity (global using) and Android.App.Activity (Android implicit usings).
using Activity = System.Diagnostics.Activity;

namespace Sentry.Internal.Tracing;

/// <summary>
/// An <see cref="ISpan"/> facade over an <see cref="Activity"/>.
/// </summary>
/// <remarks>
/// Design principle: <b>lifecycle, timing and parenting go through the Activity</b> (so the Activity is the
/// single source of truth for the span tree, and pure-OTel instrumentation interleaves naturally), while
/// <b>rich Sentry-only state delegates directly to the shadow tracer</b> that
/// <see cref="SentryActivityProcessor"/> created for the Activity (so nothing is lost squeezing Sentry
/// concepts through Activity tags). The only Sentry-native concepts that must survive the Activity lifecycle
/// boundary (an explicit <see cref="SpanStatus"/> passed to Finish) travel as side-channel values fused onto
/// the Activity, where the processor picks them up in OnEnd.
/// </remarks>
internal class ActivitySpanShim : ISpan
{
protected internal Activity Activity { get; }
private readonly ISpan _inner;
protected readonly IHub Hub;

public ActivitySpanShim(Activity activity, ISpan inner, IHub hub)
{
Activity = activity;
_inner = inner;
Hub = hub;
}

internal ISpan Inner => _inner;

// ---- Lifecycle: routed through the Activity ----

public ISpan StartChild(string operation)
{
// Parent explicitly to this shim's Activity (not Activity.Current) to preserve Sentry's
// StartChild-on-a-specific-span semantics.
var childActivity = SentryActivitySources.Shim.StartActivity(
operation, ActivityKind.Internal, Activity.Context);

if (childActivity?.GetFused<ISpan>() is { } childSpan)
{
return new ActivitySpanShim(childActivity, childSpan, Hub);
}

// No listener is running (or the activity was suppressed) - fall back to the classic path.
childActivity?.Dispose();
return _inner.StartChild(operation);
}

public void Finish()
{
// An explicit status set via the property (rather than Finish(status)) must survive OnEnd,
// which would otherwise derive the status from the Activity.
if (_inner.Status is { } status)
{
FuseStatus(status);
}
StopActivity();
}

public void Finish(SpanStatus status)
{
FuseStatus(status);
Activity.SetStatus(status == SpanStatus.Ok ? ActivityStatusCode.Ok : ActivityStatusCode.Error);
StopActivity();
}

public void Finish(Exception exception, SpanStatus status)
{
Hub.BindException(exception, _inner);
Finish(status);
}

public void Finish(Exception exception) => Finish(exception, SpanStatusConverter.FromException(exception));

public void Dispose() => Finish();

private void FuseStatus(SpanStatus status) => Activity.SetFused(ShimKeys.SpanStatus, status);

private void StopActivity()
{
if (!Activity.IsStopped)
{
Activity.Stop();
}
}

// ---- Rich state: delegated to the shadow tracer ----

public string? Description
{
get => _inner.Description;
set => _inner.Description = value;
}

public string Operation
{
get => _inner.Operation;
set => _inner.Operation = value;
}

public SpanStatus? Status
{
get => _inner.Status;
set => _inner.Status = value;
}

public SpanId SpanId => _inner.SpanId;
public SpanId? ParentSpanId => _inner.ParentSpanId;
public SentryId TraceId => _inner.TraceId;
public string? Origin => _inner.Origin;
public bool? IsSampled => _inner.IsSampled;

public DateTimeOffset StartTimestamp => _inner.StartTimestamp;
public DateTimeOffset? EndTimestamp => _inner.EndTimestamp;
public bool IsFinished => _inner.IsFinished;
public SentryTraceHeader GetTraceHeader() => _inner.GetTraceHeader();

public IReadOnlyDictionary<string, Measurement> Measurements => _inner.Measurements;
public void SetMeasurement(string name, Measurement measurement) => _inner.SetMeasurement(name, measurement);

public IReadOnlyDictionary<string, object?> Data => _inner.Data;
public void SetData(string key, object? value) => _inner.SetData(key, value);

public IReadOnlyDictionary<string, string> Tags => _inner.Tags;
public void SetTag(string key, string value) => _inner.SetTag(key, value);
public void UnsetTag(string key) => _inner.UnsetTag(key);

public IReadOnlyDictionary<string, object?> Extra => _inner.Extra;
public void SetExtra(string key, object? value) => _inner.SetExtra(key, value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// Alias required because Android TFMs of the core Sentry package otherwise see an ambiguous reference
// between System.Diagnostics.Activity (global using) and Android.App.Activity (Android implicit usings).
using Activity = System.Diagnostics.Activity;

namespace Sentry.Internal.Tracing;

/// <summary>
/// An <see cref="ITransactionTracer"/> facade over an <see cref="Activity"/>. See
/// <see cref="ActivitySpanShim"/> for the design principle. Created by <see cref="Create"/>, which
/// <c>Hub.StartTransaction</c> invokes (via <c>SentryOptions.ActivityShimFactory</c>) for Sentry-API
/// transactions when Activity-based tracing is enabled.
/// </summary>
internal sealed class ActivityTransactionShim : ActivitySpanShim, ITransactionTracer
{
private readonly ITransactionTracer _inner;

private ActivityTransactionShim(Activity activity, ITransactionTracer inner, IHub hub)
: base(activity, inner, hub)
{
_inner = inner;
}

/// <summary>
/// Starts an Activity for a Sentry-API transaction and returns a shim wrapping it, or null when no
/// activity listener is running (callers should fall back to the classic tracing path).
/// </summary>
/// <remarks>
/// Uses CreateActivity + Start (rather than StartActivity) so that side-channel values the
/// <see cref="SentryActivityProcessor"/> needs at OnStart time - the custom sampling context, an inbound
/// dynamic sampling context and the transaction name - are all in place before OnStart runs sampling.
/// </remarks>
public static ITransactionTracer? Create(
IHub hub,
ITransactionContext context,
IReadOnlyDictionary<string, object?> customSamplingContext,
DynamicSamplingContext? dynamicSamplingContext)
{
// Continue an inbound (remote) trace, propagating the upstream sampling decision.
ActivityContext parentContext = default;
if (context.ParentSpanId is { } parentSpanId && parentSpanId != default(SpanId))
{
var flags = context.IsParentSampled == true ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None;
parentContext = new ActivityContext(
context.TraceId.AsActivityTraceId(), parentSpanId.AsActivitySpanId(), flags, isRemote: true);
}

var activity = SentryActivitySources.Shim.CreateActivity(
context.Operation, ActivityKind.Internal, parentContext);
if (activity is null)
{
// No listener is subscribed to the Sentry ActivitySource.
return null;
}

activity.DisplayName = context.Name;
activity.SetFused(ShimKeys.CustomSamplingContext, customSamplingContext);
if (dynamicSamplingContext is not null)
{
activity.SetFused(dynamicSamplingContext);
}

activity.Start();

if (activity.GetFused<ISpan>() is not ITransactionTracer inner)
{
// The listener saw the activity but did not map it (e.g. the hub was disabled mid-flight).
activity.Dispose();
return null;
}

var shim = new ActivityTransactionShim(activity, inner, hub);
// The processor put the shadow tracer on the scope; replace it with the shim so that
// integrations calling hub.GetSpan()/scope.Transaction route child spans through Activities.
hub.ConfigureScope(static (scope, s) => scope.Transaction = s, shim);
return shim;
}

// ---- ITransactionTracer ----

public string Name
{
get => _inner.Name;
set
{
// The DisplayName must be kept in sync because the processor derives the transaction name
// from the Activity in OnEnd (which would otherwise clobber a rename back to the old name).
Activity.DisplayName = value;
_inner.Name = value;
}
}

public TransactionNameSource NameSource => _inner.NameSource;

public bool? IsParentSampled
{
get => _inner.IsParentSampled;
set => _inner.IsParentSampled = value;
}

public IReadOnlyCollection<ISpan> Spans => _inner.Spans;

public ISpan? GetLastActiveSpan()
{
if (_inner.GetLastActiveSpan() is not { } last)
{
return null;
}
// Every span the processor creates has its Activity fused on; wrap it so that callers
// (e.g. integrations creating children of the "current" span) route through Activities.
return last.GetFused<Activity>() is { } activity
? new ActivitySpanShim(activity, last, Hub)
: last;
}

// ---- IEventLike (delegated) ----

public IReadOnlyCollection<Breadcrumb> Breadcrumbs => _inner.Breadcrumbs;
public void AddBreadcrumb(Breadcrumb breadcrumb) => _inner.AddBreadcrumb(breadcrumb);

public string? Distribution
{
get => _inner.Distribution;
set => _inner.Distribution = value;
}

public SentryLevel? Level
{
get => _inner.Level;
set => _inner.Level = value;
}

public SentryRequest Request
{
get => _inner.Request;
set => _inner.Request = value;
}

public SentryContexts Contexts
{
get => _inner.Contexts;
set => _inner.Contexts = value;
}

public SentryUser User
{
get => _inner.User;
set => _inner.User = value;
}

public string? Release
{
get => _inner.Release;
set => _inner.Release = value;
}

public string? Environment
{
get => _inner.Environment;
set => _inner.Environment = value;
}

public string? TransactionName
{
get => _inner.TransactionName;
set => _inner.TransactionName = value;
}

public SdkVersion Sdk => _inner.Sdk;

public string? Platform
{
get => _inner.Platform;
set => _inner.Platform = value;
}

public IReadOnlyList<string> Fingerprint
{
get => _inner.Fingerprint;
set => _inner.Fingerprint = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace Sentry.Internal.Tracing;
internal sealed class SentryActivityListener : IDisposable
{
private readonly ActivityListener _listener;
private readonly SentryOptions? _options;

internal SentryActivityProcessor Processor { get; }

Expand All @@ -48,7 +49,23 @@ public SentryActivityListener(
ActivityStopped = Processor.OnEnd
};
ActivitySource.AddActivityListener(_listener);

// Install the tracing shim: Sentry-API transactions (SentrySdk.StartTransaction et al.) get routed
// through Activities on the Sentry ActivitySource, making the Activity the single source of truth
// for spans regardless of which API created them.
_options = hub.GetSentryOptions();
if (_options is not null)
{
_options.ActivityShimFactory = ActivityTransactionShim.Create;
}
}

public void Dispose() => _listener.Dispose();
public void Dispose()
{
if (_options is { ActivityShimFactory: { } factory } && factory == ActivityTransactionShim.Create)
{
_options.ActivityShimFactory = null;
}
_listener.Dispose();
}
}
Loading
Loading