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
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using Sentry.Internal.Extensions;
using Sentry.Internal.OpenTelemetry;

namespace Sentry.Internal.Tracing;

// Ported from Sentry.OpenTelemetry.OpenTelemetryExtensions so that the core Activity processor has no
// dependency on the OpenTelemetry SDK package.
internal static class ActivityAttributeExtensions
{
public static BaggageHeader AsBaggageHeader(this IEnumerable<KeyValuePair<string, string?>> baggage,
bool useSentryPrefix = false) =>
BaggageHeader.Create(
baggage.Where(member => member.Value != null)
.Select(kvp => (KeyValuePair<string, string>)kvp!),
useSentryPrefix
);

/// <summary>
/// The names that OpenTelemetry gives to attributes, by convention, have changed over time so we often need to
/// check for both the new attribute and any obsolete ones.
/// </summary>
private static T? GetFirstMatchingAttribute<T>(this IDictionary<string, object?> attributes,
params string[] attributeNames)
{
foreach (var name in attributeNames)
{
if (attributes.TryGetTypedValue(name, out T value))
{
return value;
}
}

return default;
}

public static string? HttpMethodAttribute(this IDictionary<string, object?> attributes) =>
attributes.GetFirstMatchingAttribute<string>(
OtelSemanticConventions.AttributeHttpRequestMethod,
OtelSemanticConventions.AttributeHttpMethod // Fallback pre-1.5.0
);

public static string? UrlFullAttribute(this IDictionary<string, object?> attributes) =>
attributes.GetFirstMatchingAttribute<string>(
OtelSemanticConventions.AttributeUrlFull,
OtelSemanticConventions.AttributeHttpUrl // Fallback pre-1.5.0
);

public static short? HttpResponseStatusCodeAttribute(this IDictionary<string, object?> attributes)
{
var statusCode = attributes.GetFirstMatchingAttribute<int?>(
OtelSemanticConventions.AttributeHttpResponseStatusCode
);
return statusCode is >= short.MinValue and <= short.MaxValue
? (short)statusCode.Value
: null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace Sentry.Internal.Tracing;

// Note: a copy of these conversions ships in Sentry.OpenTelemetry(.Exporter) as Sentry.Internal.ActivityExtensions.
// This copy lives in a distinct namespace to avoid CS0436 conflicts in the OTel packages, which can see Sentry's
// internals via InternalsVisibleTo. The copies converge when the OTel packages are rebased onto core (see spike notes).
internal static class ActivityIdExtensions
{
private const int HexCharsPerByte = 2;
private const int SpanIdByteCount = sizeof(long);
private const int SpanIdHexCharCount = SpanIdByteCount * HexCharsPerByte;
private static readonly int TraceIdByteCount = Unsafe.SizeOf<Guid>();
internal static readonly int TraceIdHexCharCount = TraceIdByteCount * HexCharsPerByte;

public static SpanId AsSentrySpanId(this ActivitySpanId id) => SpanId.Parse(id.ToHexString());

public static ActivitySpanId AsActivitySpanId(this SpanId id)
{
#if NET8_0_OR_GREATER
Span<byte> buffer = stackalloc byte[SpanIdByteCount];
id.TryWriteBytes(buffer);
return ActivitySpanId.CreateFromBytes(buffer);
#else
Span<char> buffer = stackalloc char[SpanIdHexCharCount];
id.TryFormat(buffer);
return ActivitySpanId.CreateFromString(buffer);
#endif
}

public static SentryId AsSentryId(this ActivityTraceId id) => SentryId.Parse(id.ToHexString());

public static ActivityTraceId AsActivityTraceId(this SentryId id)
{
#if NET8_0_OR_GREATER
Span<byte> buffer = stackalloc byte[TraceIdByteCount];
id.TryWriteBytes(buffer);
return ActivityTraceId.CreateFromBytes(buffer);
#else
Span<char> buffer = stackalloc char[TraceIdHexCharCount];
id.TryFormat(buffer);
return ActivityTraceId.CreateFromString(buffer);
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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>
/// Enriches Sentry spans with additional information from the <see cref="Activity"/> that produced them,
/// just before the span is finished. Core equivalent of Sentry.OpenTelemetry.IOpenTelemetryEnricher.
/// </summary>
internal interface ISentryActivityEnricher
{
public void Enrich(ISpan span, Activity activity, IHub hub, SentryOptions? options);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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>
/// Subscribes to <see cref="Activity"/> instrumentation via <see cref="ActivityListener"/> (part of the .NET
/// runtime — no OpenTelemetry SDK dependency) and forwards activity lifecycle events to a
/// <see cref="SentryActivityProcessor"/>, which converts them into Sentry transactions and spans.
/// </summary>
/// <remarks>
/// This replaces the registration glue that Sentry.OpenTelemetry gets from the OpenTelemetry SDK
/// (TracerProviderBuilder.AddProcessor): where the OTel SDK decides which ActivitySources to listen to and
/// which activities to sample, here those decisions are made by the <c>shouldListenTo</c> predicate and the
/// Sample callback respectively.
///
/// Spike note on sampling: for parity with the current POTEL behaviour (where the OTel SDK records everything
/// and Sentry re-runs its own sampling when the root span is converted in
/// <c>SentryActivityProcessor.CreateRootSpan</c>), the Sample callback returns AllDataAndRecorded for
/// every activity. A production implementation could instead invoke Sentry's sampling logic here — at activity
/// creation time — to avoid paying for recording of activities that Sentry will discard. That requires solving
/// the TracesSampler customSamplingContext gap (ActivityCreationOptions has no channel for it) and reconciling
/// with any other ActivityListeners present (the runtime takes the most permissive sampling result).
/// </remarks>
internal sealed class SentryActivityListener : IDisposable
{
private readonly ActivityListener _listener;

internal SentryActivityProcessor Processor { get; }

public SentryActivityListener(
IHub hub,
Func<ActivitySource, bool>? shouldListenTo = null,
IEnumerable<ISentryActivityEnricher>? enrichers = null,
IReplaySession? replaySession = null,
Func<IDictionary<string, object>>? resourceAttributeResolver = null)
{
Processor = new SentryActivityProcessor(hub, enrichers, replaySession, resourceAttributeResolver);
_listener = new ActivityListener
{
ShouldListenTo = source => shouldListenTo?.Invoke(source) ?? true,
Sample = static (ref ActivityCreationOptions<ActivityContext> _) =>
ActivitySamplingResult.AllDataAndRecorded,
SampleUsingParentId = static (ref ActivityCreationOptions<string> _) =>
ActivitySamplingResult.AllDataAndRecorded,
ActivityStarted = Processor.OnStart,
ActivityStopped = Processor.OnEnd
};
ActivitySource.AddActivityListener(_listener);
}

public void Dispose() => _listener.Dispose();
}
Loading
Loading