diff --git a/CHANGELOG.md b/CHANGELOG.md index dbbcb63b73..5fe72d5117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ ### API changes - Removed `SentryOptionsExtensions` class - all the public methods moved directly to `SentryOptions` ([#3195](https://github.com/getsentry/sentry-dotnet/pull/3195)) - +- Sentry now uses System.Diagnostics.DiagnosticSource internally for tracing by default when targeting .NET 6 or later, meaning any traces created by Sentry can be captured via OpenTelemetry. If you are using .NET Core 3.1 or earlier, you can opt into this via the `Sentry.DiagnosticSource` package. ([#3238](https://github.com/getsentry/sentry-dotnet/pull/3238)) + ### Dependencies - Bump CLI from v2.30.0 to v2.30.2 ([#3214](https://github.com/getsentry/sentry-dotnet/pull/3214), [#3218](https://github.com/getsentry/sentry-dotnet/pull/3218)) diff --git a/modules/sentry-native b/modules/sentry-native index 4ec95c0725..9bc0fc75e3 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit 4ec95c0725df5f34440db8fa8d37b4c519fce74e +Subproject commit 9bc0fc75e34cb43e7019d76b0decb6c0cddbfd34 diff --git a/src/Sentry.DiagnosticSource/Internal/Tracing/ActivitySpanProcessor.cs b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivitySpanProcessor.cs new file mode 100644 index 0000000000..9258e080eb --- /dev/null +++ b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivitySpanProcessor.cs @@ -0,0 +1,470 @@ +using Sentry.Extensibility; +using Sentry.Internal.Extensions; +using Sentry.Internal.OpenTelemetry; + +namespace Sentry.Internal.Tracing; + +/// +/// Helper class to convert events to Sentry Spans. +/// +internal class ActivitySpanProcessor +{ + private readonly IHub _hub; + + private Action? _beforeFinish; + private readonly Instrumenter _instrumenter; + + // ReSharper disable once MemberCanBePrivate.Global - Used by tests + internal readonly ConcurrentDictionary _map = new(); + private readonly SentryOptions? _options; + private readonly Lazy> _resourceAttributes; + + private static readonly long PruningInterval = TimeSpan.FromSeconds(5).Ticks; + internal long _lastPruned = 0; + private readonly Lazy _realHub; + + static ActivitySpanProcessor() + { +#if !NET5_0_OR_GREATER + if (Activity.DefaultIdFormat == ActivityIdFormat.W3C) + { + return; + } + + // TODO: Could customers potentially be relying on the Hierarchical format? If so, this will get us in trouble. + // + // Another option would be to warn customers and have them set this themselves (that would be more deliberate). + // + // Finally, we can override the ActivityIdFormat for each trace by using ActivitySource.CreateActivity instead + // of ActivitySource.StartActivity. That's a bit more fragile and will only work for spans created by the SDK + // (not for anything users instrument themselves). + // + // Activity.SpanId only gets a non-zero value if the activity ID format is W3C (the default since net5.0). The + // default is Hierarchical on .NET Framework, .NET Core 3.1 and below, which won't work with Sentry tracing. + Debug.WriteLine("Setting Activity.DefaultIdFormat to W3C."); + Activity.DefaultIdFormat = ActivityIdFormat.W3C; +#endif + } + + internal ActivitySpanProcessor(IHub hub, Instrumenter instrumenter) + : this(hub, null, null, instrumenter) + { + } + + internal ActivitySpanProcessor(IHub hub, Action? beforeFinish, + Func>? resourceAttributeResolver) + : this(hub, beforeFinish, resourceAttributeResolver, Instrumenter.OpenTelemetry) + { + } + + private ActivitySpanProcessor(IHub hub, Action? beforeFinish, + Func>? resourceAttributeResolver, Instrumenter instrumenter) + { + _hub = hub; + _beforeFinish = beforeFinish; + _instrumenter = instrumenter; + _realHub = new Lazy(() => + _hub switch + { + Hub thisHub => thisHub, + HubAdapter when SentrySdk.CurrentHub is Hub sdkHub => sdkHub, + _ => null + }); + + _options = hub.GetSentryOptions(); + + if (_options is null) + { + throw new InvalidOperationException( + "The Sentry SDK has not been initialised. To use tracing you need to initialize the Sentry SDK."); + } + + // Resource attributes are consistent between spans, but not available during construction. + // Thus, get a single instance lazily. + resourceAttributeResolver ??= () => new Dictionary(); + _resourceAttributes = new Lazy>(resourceAttributeResolver); + } + + internal ISpan? GetMappedSpan(ActivitySpanId spanId) => _map.GetValueOrDefault(spanId); + + /// + public void OnStart(System.Diagnostics.Activity data) + { + if (data.ParentSpanId != default && _map.TryGetValue(data.ParentSpanId, out var parentSpan)) + { + // We can find the parent span - start a child span. + var context = new SpanContext( + data.OperationName, + data.SpanId.AsSentrySpanId(), + data.ParentSpanId.AsSentrySpanId(), + data.TraceId.AsSentryId(), + data.DisplayName, + null, + null) + { + Instrumenter = _instrumenter + }; + + var span = (SpanTracer)parentSpan.StartChild(context); + span.StartTimestamp = data.StartTimeUtc; + // Used to filter out spans that are not recorded when finishing a transaction. + data.BindSentrySpan(span); + span.IsFiltered = () => span.GetActivity() + is { IsAllDataRequested: false, Recorded: false }; + _map[data.SpanId] = span; + } + else + { +#if HAS_DIAGNOSTICS_7_OR_GREATER + // If a parent exists, then copy its sampling decision. + bool? isSampled = data.HasRemoteParent ? data.Recorded : null; +#else + bool? isSampled = null; +#endif + + // No parent span found - start a new transaction + var transactionContext = new TransactionContext(data.DisplayName, + data.OperationName, + data.SpanId.AsSentrySpanId(), + data.ParentSpanId.AsSentrySpanId(), + data.TraceId.AsSentryId(), + data.DisplayName, null, isSampled, isSampled) + { + Instrumenter = _instrumenter + }; + + var baggageHeader = data.Baggage.AsBaggageHeader(); + var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext(); + var transaction = (TransactionTracer)_hub.StartTransaction( + transactionContext, new Dictionary(), dynamicSamplingContext + ); + transaction.StartTimestamp = data.StartTimeUtc; + _hub.ConfigureScope(scope => scope.Transaction = transaction); + data.BindSentrySpan(transaction); + _map[data.SpanId] = transaction; + } + + // Housekeeping + PruneFilteredSpans(); + } + + public void OnEnd(System.Diagnostics.Activity data) + { + // Make a dictionary of the attributes (aka "tags") for faster lookup when used throughout the processor. + var attributes = data.TagObjects.ToDict(); + + var url = + attributes.TryGetTypedValue(OtelSemanticConventions.AttributeUrlFull, out string? tempUrl) ? tempUrl + : attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpUrl, out string? fallbackUrl) ? fallbackUrl // Falling back to pre-1.5.0 + : null; + + if (!string.IsNullOrEmpty(url) && (_options?.IsSentryRequest(url) ?? false)) + { + _options?.DiagnosticLogger?.LogDebug($"Ignoring Activity {data.SpanId} for Sentry request."); + + if (_map.TryRemove(data.SpanId, out var removed)) + { + if (removed is SpanTracer spanTracerToRemove) + { + spanTracerToRemove.IsSentryRequest = true; + } + + if (removed is TransactionTracer transactionTracer) + { + transactionTracer.IsSentryRequest = true; + } + } + + return; + } + + if (!_map.TryGetValue(data.SpanId, out var span)) + { + _options?.DiagnosticLogger?.LogError($"Span not found for SpanId: {data.SpanId}. Did OnStart run? We might have a bug in the SDK."); + return; + } + + var (operation, description, source) = ParseOtelSpanDescription(data, attributes); + span.Operation = operation; + span.Description = description; + + if (span is TransactionTracer transaction) + { + transaction.Name = description; + transaction.NameSource = source; + + // Use the end timestamp from the activity data. + transaction.EndTimestamp = data.StartTimeUtc + data.Duration; + + // Transactions set otel attributes (and resource attributes) as context. + transaction.Contexts["otel"] = GetOtelContext(attributes); + } + else + { + // Use the end timestamp from the activity data. + ((SpanTracer)span).EndTimestamp = data.StartTimeUtc + data.Duration; + + // Spans set otel attributes in extras (passed to Sentry as "data" on the span). + // Resource attributes do not need to be set, as they would be identical as those set on the transaction. + span.SetExtras(attributes); + span.SetExtra("otel.kind", data.Kind); + } + + // In ASP.NET Core the middleware finishes up (and the scope gets popped) before the activity is ended. So we + // need to restore the scope here (it's saved by our middleware when the request starts) + var activityScope = GetSavedScope(data); + if (activityScope is { } savedScope) + { + var hub = _realHub.Value; + hub?.RestoreScope(savedScope); + } + GenerateSentryErrorsFromOtelSpan(data, attributes); + _beforeFinish?.Invoke(span, data); + if (data.GetException() is { } exception) + { + span.Finish(exception); + } + else + { + // TODO: Does this override a status that we might be setting manually? This logic worked for OTel spans but + // might need to be more sophisticated for ActivityTraceSpans... alternatively we need to be more + // sophisticated about how we set status in the first place (leveraging attributes that will be applied + // appropriately by this GetSpanStatus method). + var status = GetSpanStatus(data.Status, attributes); + span.Finish(status); + } + + _map.TryRemove(data.SpanId, out _); + + // Housekeeping + PruneFilteredSpans(); + } + + /// + /// Clean up items that may have been filtered out. + /// See https://github.com/getsentry/sentry-dotnet/pull/3198 + /// + internal void PruneFilteredSpans(bool force = false) + { + if (!force && !NeedsPruning()) + { + return; + } + + foreach (var mappedItem in _map) + { + var (spanId, span) = mappedItem; + if (span.GetActivity() is { Recorded: false, IsAllDataRequested: false }) + { + _map.TryRemove(spanId, out _); + } + } + } + + private bool NeedsPruning() + { + var lastPruned = Interlocked.Read(ref _lastPruned); + if (lastPruned > DateTime.UtcNow.Ticks - PruningInterval) + { + return false; + } + + var thisPruned = DateTime.UtcNow.Ticks; + Interlocked.CompareExchange(ref _lastPruned, thisPruned, lastPruned); + // May be false if another thread gets there first + return Interlocked.Read(ref _lastPruned) == thisPruned; + } + + private static Scope? GetSavedScope(System.Diagnostics.Activity? activity) + { + while (activity is not null) + { + if (activity.GetFused() is { } savedScope) + { + return savedScope; + } + activity = activity.Parent; + } + return null; + } + + internal static SpanStatus GetSpanStatus(ActivityStatusCode status, IDictionary attributes) + { + // See https://github.com/open-telemetry/opentelemetry-dotnet/discussions/4703 + if (attributes.TryGetValue(OtelSpanAttributeConstants.StatusCodeKey, out var statusCode) + && statusCode is OtelStatusTags.ErrorStatusCodeTagValue + ) + { + return GetErrorSpanStatus(attributes); + } + return status switch + { + ActivityStatusCode.Unset => SpanStatus.Ok, + ActivityStatusCode.Ok => SpanStatus.Ok, + ActivityStatusCode.Error => GetErrorSpanStatus(attributes), + _ => SpanStatus.UnknownError + }; + } + + private static SpanStatus GetErrorSpanStatus(IDictionary attributes) + { + if (attributes.TryGetTypedValue("http.status_code", out int httpCode)) + { + return SpanStatusConverter.FromHttpStatusCode(httpCode); + } + + if (attributes.TryGetTypedValue("rpc.grpc.status_code", out int grpcCode)) + { + return SpanStatusConverter.FromGrpcStatusCode(grpcCode); + } + + return SpanStatus.UnknownError; + } + + private static (string operation, string description, TransactionNameSource source) ParseOtelSpanDescription( + System.Diagnostics.Activity activity, + IDictionary attributes) + { + // This function should loosely match the JavaScript implementation at: + // https://github.com/getsentry/sentry-javascript/blob/3487fa3af7aa72ac7fdb0439047cb7367c591e77/packages/opentelemetry-node/src/utils/parseOtelSpanDescription.ts + // However, it should also follow the OpenTelemetry semantic conventions specification, as indicated. + + // HTTP span + // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/http/ + if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpMethod, out string httpMethod)) + { + if (activity.Kind == ActivityKind.Client) + { + // Per OpenTelemetry spec, client spans use only the method. + return ("http.client", httpMethod, TransactionNameSource.Custom); + } + + if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpRoute, out string httpRoute)) + { + // A route exists. Use the method and route. + return ("http.server", $"{httpMethod} {httpRoute}", TransactionNameSource.Route); + } + + if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpTarget, out string httpTarget)) + { + // A target exists. Use the method and target. If the target is "/" we can treat it like a route. + var source = httpTarget == "/" ? TransactionNameSource.Route : TransactionNameSource.Url; + return ("http.server", $"{httpMethod} {httpTarget}", source); + } + + // Some other type of HTTP server span. Pass it through with the original name. + return ("http.server", activity.DisplayName, TransactionNameSource.Custom); + } + + // DB span + // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/database/ + if (attributes.ContainsKey(OtelSemanticConventions.AttributeDbSystem)) + { + if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeDbStatement, out string dbStatement)) + { + // We have a database statement. Use it. + return ("db", dbStatement, TransactionNameSource.Task); + } + + // Some other type of DB span. Pass it through with the original name. + return ("db", activity.DisplayName, TransactionNameSource.Task); + } + + // RPC span + // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/rpc/ + if (attributes.ContainsKey(OtelSemanticConventions.AttributeRpcService)) + { + return ("rpc", activity.DisplayName, TransactionNameSource.Route); + } + + // Messaging span + // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/messaging/ + if (attributes.ContainsKey(OtelSemanticConventions.AttributeMessagingSystem)) + { + return ("message", activity.DisplayName, TransactionNameSource.Route); + } + + // FaaS (Functions/Lambda) span + // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/faas/ + if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeFaasTrigger, out string faasTrigger)) + { + return (faasTrigger, activity.DisplayName, TransactionNameSource.Route); + } + + // Default - pass through unmodified. + return (activity.OperationName, activity.DisplayName, TransactionNameSource.Custom); + } + + private Dictionary GetOtelContext(IDictionary attributes) + { + var otelContext = new Dictionary(); + if (attributes.Count > 0) + { + otelContext.Add("attributes", attributes); + } + + var resourceAttributes = _resourceAttributes.Value; + if (resourceAttributes.Count > 0) + { + otelContext.Add("resource", resourceAttributes); + } + + return otelContext; + } + + private void GenerateSentryErrorsFromOtelSpan(System.Diagnostics.Activity activity, IDictionary spanAttributes) + { + // https://develop.sentry.dev/sdk/performance/opentelemetry/#step-7-define-generatesentryerrorsfromotelspan + // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/exceptions/ + foreach (var @event in activity.Events.Where(e => e.Name == OtelSemanticConventions.AttributeExceptionEventName)) + { + var eventAttributes = @event.Tags.ToDict(); + // This would be where we would ideally implement full exception capture. That's not possible at the + // moment since the full exception isn't yet available via the OpenTelemetry API. + // See https://github.com/open-telemetry/opentelemetry-dotnet/issues/2439#issuecomment-1577314568 + // if (!eventAttributes.TryGetTypedValue("exception", out Exception exception)) + // { + // continue; + // } + + // At the moment, OTEL only gives us `exception.type`, `exception.message`, and `exception.stacktrace`... + // So the best we can do is a poor man's exception (no accurate symbolication or anything) + if (!eventAttributes.TryGetTypedValue(OtelSemanticConventions.AttributeExceptionType, out string exceptionType)) + { + continue; + } + eventAttributes.TryGetTypedValue(OtelSemanticConventions.AttributeExceptionMessage, out string message); + eventAttributes.TryGetTypedValue(OtelSemanticConventions.AttributeExceptionStacktrace, out string stackTrace); + + Exception exception; + try + { + var type = Type.GetType(exceptionType)!; + exception = (Exception)Activator.CreateInstance(type, message)!; + exception.SetSentryMechanism("SentrySpanProcessor.ErrorSpan"); + } + catch + { + _options?.DiagnosticLogger?.LogError($"Failed to create poor man's exception for type : {exceptionType}"); + continue; + } + + // TODO: Validate that our `DuplicateEventDetectionEventProcessor` prevents this from doubling exceptions + // that are also caught by other means, such as our AspNetCore middleware, etc. + // (When options.RecordException = true is set on AddAspNetCoreInstrumentation...) + // Also, in such cases - how will we get the otel scope and trace context on the other one? + + var sentryEvent = new SentryEvent(exception, @event.Timestamp); + var otelContext = GetOtelContext(spanAttributes); + otelContext.Add("stack_trace", stackTrace); + sentryEvent.Contexts["otel"] = otelContext; + _hub.CaptureEvent(sentryEvent, scope => + { + var trace = scope.Contexts.Trace; + trace.SpanId = activity.SpanId.AsSentrySpanId(); + trace.ParentSpanId = activity.ParentSpanId.AsSentrySpanId(); + trace.TraceId = activity.TraceId.AsSentryId(); + }); + } + } +} diff --git a/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTraceProvider.cs b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTraceProvider.cs new file mode 100644 index 0000000000..740754c8ee --- /dev/null +++ b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTraceProvider.cs @@ -0,0 +1,15 @@ +namespace Sentry.Internal.Tracing; + +/// +/// The concrete implementation of that uses +/// and from the +/// namespace to implement tracing. +/// +internal class ActivityTraceProvider : ITraceProvider +{ + private Lazy> _lazyActivitySources = new(); + private ConcurrentDictionary _activitySources => _lazyActivitySources.Value; + + public ITracer GetTracer(string name, string? version = "") + => _activitySources.GetOrAdd(name, new ActivityTracer(name, version)); +} diff --git a/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTraceSpan.cs b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTraceSpan.cs new file mode 100644 index 0000000000..a7c4e2ec06 --- /dev/null +++ b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTraceSpan.cs @@ -0,0 +1,74 @@ +namespace Sentry.Internal.Tracing; + +#if !NET6_0_OR_GREATER +using System.Diagnostics; +#endif + +/// +/// Wraps the functionality that we use from Activity in an interface so that we can +/// access this from our integrations without taking a hard dependency on +/// System.Diagnostics.Activity (which is only available in .NET 5.0 and later) +/// +internal class ActivityTraceSpan(System.Diagnostics.Activity activity) : ITraceSpan +{ + public string? Description => activity.DisplayName; + + public ITraceSpan AddEvent(string message) + { + activity.AddEvent(new ActivityEvent(message)); + return this; + } + + public ITraceSpan SetAttribute(string key, object value) + { + activity.SetTag(key, value); + return this; + } + + public ITraceSpan SetDescription(string? description) + { + if (description != null) + { + activity.DisplayName = description; + } + return this; + } + + public ITraceSpan SetStatus(SpanStatus status, string? description = default) + { + if (status == SpanStatus.Ok) + { + activity.SetStatus(ActivityStatusCode.Ok); + return this; + } + var errorMessage = description ?? status.ToString(); + activity.SetStatus(ActivityStatusCode.Error, errorMessage); + return this; + } + + public ITraceSpan Stop() + { + activity.Stop(); + return this; + } + + public ITraceSpan SetExtra(string key, object? value) + { + // TODO: Not sure what we want to do about Extra. This would change that data from being stored as "Extra" data + // (which is documented as being obsolete) to being stored as tags... which is a change in behaviour. The docs + // say structured contexts should be used instead, so maybe we need to create some new structured contexts for + // this "arbitrary" extra data that both we and users are currently storing. Alternatively, we can continue to + // store it as extra (in which case we need to implement this in some kind of custom property both here and when + // converting the activity into a SentrySpan for transmission) + activity.SetTag(key, value); + return this; + } + + public ITraceSpan Finish(Exception exception) + { + activity.BindException(exception); + return Stop(); + } + + public void Dispose() => activity.Dispose(); +} diff --git a/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTracer.cs b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTracer.cs new file mode 100644 index 0000000000..bb12e5d087 --- /dev/null +++ b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTracer.cs @@ -0,0 +1,27 @@ +namespace Sentry.Internal.Tracing; + +/// +/// Wraps the functionality that we use from ActivitySource in an interface so that we +/// can access this from our integrations without taking a hard dependency on +/// System.Diagnostics.ActivitySource (which is only available in .NET 5.0 and later) +/// +internal class ActivityTracer(string name, string? version = "") : ITracer +{ + private readonly ActivitySource _activitySource = new(name, version); + + public ITraceSpan? StartSpan(string operationName, string? description = null) + { + var activity = _activitySource.CreateActivity(operationName, ActivityKind.Internal) + ?.SetIdFormat(ActivityIdFormat.W3C) + ?.Start(); + if (activity is not null) + { + activity.DisplayName = description ?? operationName; + } + return activity is not null ? new ActivityTraceSpan(activity) : null; + } + + public ITraceSpan? CurrentSpan => System.Diagnostics.Activity.Current == null + ? null + : new ActivityTraceSpan(System.Diagnostics.Activity.Current); +} diff --git a/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTracingIntegration.cs b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTracingIntegration.cs new file mode 100644 index 0000000000..68e33ca105 --- /dev/null +++ b/src/Sentry.DiagnosticSource/Internal/Tracing/ActivityTracingIntegration.cs @@ -0,0 +1,29 @@ +using Sentry.Extensibility; +using Sentry.Integrations; + +namespace Sentry.Internal.Tracing; + +internal class ActivityTracingIntegration : ISdkIntegration +{ + /* + TODO: Think about where to put this... would be good if it sat on an IDisposable but also something internal. + We could possibly put it on the Hub - add an ITracingHub interface that was internal and held both this and + the SentryTraceProvider. Alternatively we could rename IMetricHub to IInternalHub and put it there... metrics + should probably be using this mechanism for tracing anyway (as it's all internal) but that would complicate + things like code locations... which we could maybe store as custom properties on the Activity. + */ + private SentryActivityListener? _listener; + + public void Register(IHub hub, SentryOptions options) + { + if (!options.IsPerformanceMonitoringEnabled) + { + options.Log(SentryLevel.Info, "SentryTracing Integration is disabled because tracing is disabled."); + return; + } + + // TODO: Should we be registering this if OpenTelemetry is enabled? + options.InternalTraceProvider = new ActivityTraceProvider(); + _listener = new SentryActivityListener(hub, Instrumenter.ActivitySource); + } +} diff --git a/src/Sentry.DiagnosticSource/Internal/Tracing/SentryActivityListener.cs b/src/Sentry.DiagnosticSource/Internal/Tracing/SentryActivityListener.cs new file mode 100644 index 0000000000..ebfa8f7a90 --- /dev/null +++ b/src/Sentry.DiagnosticSource/Internal/Tracing/SentryActivityListener.cs @@ -0,0 +1,44 @@ +namespace Sentry.Internal.Tracing; + +internal class SentryActivityListener : IDisposable +{ + private readonly ActivitySpanProcessor _activitySpanProcessor; + private readonly ActivityListener? _listener; + + public SentryActivityListener(IHub hub, Instrumenter instrumenter) : this(new ActivitySpanProcessor(hub, instrumenter)) + { + } + + public SentryActivityListener(ActivitySpanProcessor activitySpanProcessor) + { + _activitySpanProcessor = activitySpanProcessor; + _listener = new ActivityListener() + { + // This is only for internal Sentry events + ShouldListenTo = (source) => source.Name.StartsWith("Sentry"), + Sample = ShouldSample, + ActivityStarted = OnActivityStarted, + ActivityStopped = OnActivityStopped + }; + ActivitySource.AddActivityListener(_listener); + } + + // We sample all the activities... these will get filtered out by the Hub when they get converted to spans + public ActivitySamplingResult ShouldSample(ref ActivityCreationOptions _) + => ActivitySamplingResult.AllDataAndRecorded; + + public void OnActivityStarted(System.Diagnostics.Activity activity) + { + _activitySpanProcessor.OnStart(activity); + } + + public void OnActivityStopped(System.Diagnostics.Activity activity) + { + _activitySpanProcessor.OnEnd(activity); + } + + public void Dispose() + { + _listener?.Dispose(); + } +} diff --git a/src/Sentry.DiagnosticSource/Internal/Tracing/TraceExtensions.cs b/src/Sentry.DiagnosticSource/Internal/Tracing/TraceExtensions.cs new file mode 100644 index 0000000000..2b4c32b94a --- /dev/null +++ b/src/Sentry.DiagnosticSource/Internal/Tracing/TraceExtensions.cs @@ -0,0 +1,41 @@ +namespace Sentry.Internal.Tracing; + +internal static class TraceExtensions +{ + private const string SentrySpanKey = "_Sentry.SentrySpan"; + private const string SentryExceptionKey = "_Sentry.Exception"; + + public static SpanId AsSentrySpanId(this ActivitySpanId id) => SpanId.Parse(id.ToHexString()); + + public static ActivitySpanId AsActivitySpanId(this SpanId id) => ActivitySpanId.CreateFromString(id.ToString().AsSpan()); + + public static SentryId AsSentryId(this ActivityTraceId id) => SentryId.Parse(id.ToHexString()); + + public static ActivityTraceId AsActivityTraceId(this SentryId id) => ActivityTraceId.CreateFromString(id.ToString().AsSpan()); + + public static BaggageHeader AsBaggageHeader(this IEnumerable> baggage, bool useSentryPrefix = false) => + BaggageHeader.Create( + baggage.Where(member => member.Value != null) + .Select(kvp => (KeyValuePair)kvp!), + useSentryPrefix + ); + + public static void BindException(this System.Diagnostics.Activity activity, Exception exception) + { + activity.SetCustomProperty(SentryExceptionKey, exception); + } + + public static Exception? GetException(this System.Diagnostics.Activity activity) => + activity.GetCustomProperty(SentryExceptionKey) as Exception; + + public static void BindSentrySpan(this System.Diagnostics.Activity activity, ISpan span) + { + activity.SetCustomProperty(SentrySpanKey, span); + span.SetFused(activity); // We use a weak reference to allow the Activity to be disposed + } + + public static ISpan? GetSentrySpan(this System.Diagnostics.Activity activity) + => activity.GetCustomProperty(SentrySpanKey) is ISpan span ? span : null; + + public static System.Diagnostics.Activity? GetActivity(this ISpan span) => span.GetFused(); +} diff --git a/src/Sentry.DiagnosticSource/Sentry.DiagnosticSource.csproj b/src/Sentry.DiagnosticSource/Sentry.DiagnosticSource.csproj index 2bb043f837..1c7bcbc65b 100644 --- a/src/Sentry.DiagnosticSource/Sentry.DiagnosticSource.csproj +++ b/src/Sentry.DiagnosticSource/Sentry.DiagnosticSource.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Sentry.Extensions.Logging/SentryHttpMessageHandlerBuilderFilter.cs b/src/Sentry.Extensions.Logging/SentryHttpMessageHandlerBuilderFilter.cs index f35cad05d9..22a2af3c0c 100644 --- a/src/Sentry.Extensions.Logging/SentryHttpMessageHandlerBuilderFilter.cs +++ b/src/Sentry.Extensions.Logging/SentryHttpMessageHandlerBuilderFilter.cs @@ -17,7 +17,7 @@ public Action Configure(Action h is SentryHttpMessageHandler)) { handlerBuilder.AdditionalHandlers.Add( - new SentryHttpMessageHandler(hub) + new SentryHttpMessageHandler(hub, default, default, null, true) ); } diff --git a/src/Sentry.OpenTelemetry/OpenTelemetryExtensions.cs b/src/Sentry.OpenTelemetry/OpenTelemetryExtensions.cs deleted file mode 100644 index fecc538723..0000000000 --- a/src/Sentry.OpenTelemetry/OpenTelemetryExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Sentry.OpenTelemetry; - -internal static class OpenTelemetryExtensions -{ - public static SpanId AsSentrySpanId(this ActivitySpanId id) => SpanId.Parse(id.ToHexString()); - - public static ActivitySpanId AsActivitySpanId(this SpanId id) => ActivitySpanId.CreateFromString(id.ToString().AsSpan()); - - public static SentryId AsSentryId(this ActivityTraceId id) => SentryId.Parse(id.ToHexString()); - - public static ActivityTraceId AsActivityTraceId(this SentryId id) => ActivityTraceId.CreateFromString(id.ToString().AsSpan()); - - public static BaggageHeader AsBaggageHeader(this IEnumerable> baggage, bool useSentryPrefix = false) => - BaggageHeader.Create( - baggage.Where(member => member.Value != null) - .Select(kvp => (KeyValuePair)kvp!), - useSentryPrefix - ); -} diff --git a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs index 4ab485dde1..b8580014c4 100644 --- a/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs +++ b/src/Sentry.OpenTelemetry/OpenTelemetryTransactionProcessor.cs @@ -1,4 +1,5 @@ using Sentry.Extensibility; +using Sentry.Internal.Tracing; namespace Sentry.OpenTelemetry; diff --git a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj index 66a780f05a..46c13b97f3 100644 --- a/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj +++ b/src/Sentry.OpenTelemetry/Sentry.OpenTelemetry.csproj @@ -25,4 +25,22 @@ + + + $(DefineConstants);HAS_DIAGNOSTICS_7_OR_GREATER + + + + + + Internal\Tracing\%(RecursiveDir)%(Filename)%(Extension) + + + + diff --git a/src/Sentry.OpenTelemetry/SentryPropagator.cs b/src/Sentry.OpenTelemetry/SentryPropagator.cs index 78fd960d6b..f803dcda6c 100644 --- a/src/Sentry.OpenTelemetry/SentryPropagator.cs +++ b/src/Sentry.OpenTelemetry/SentryPropagator.cs @@ -2,6 +2,7 @@ using OpenTelemetry; using OpenTelemetry.Context.Propagation; using Sentry.Extensibility; +using Sentry.Internal.Tracing; namespace Sentry.OpenTelemetry; diff --git a/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs b/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs index 520c09fbd8..d9db8daee3 100644 --- a/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs +++ b/src/Sentry.OpenTelemetry/SentrySpanProcessor.cs @@ -1,8 +1,7 @@ using OpenTelemetry; using Sentry.Extensibility; -using Sentry.Internal; using Sentry.Internal.Extensions; -using Sentry.Internal.OpenTelemetry; +using Sentry.Internal.Tracing; namespace Sentry.OpenTelemetry; @@ -11,17 +10,11 @@ namespace Sentry.OpenTelemetry; /// public class SentrySpanProcessor : BaseProcessor { - private readonly IHub _hub; internal readonly IEnumerable _enrichers; - // ReSharper disable once MemberCanBePrivate.Global - Used by tests - internal readonly ConcurrentDictionary _map = new(); + private readonly ActivitySpanProcessor _activitySpanProcessor; + private readonly IHub _hub; private readonly SentryOptions? _options; - private readonly Lazy> _resourceAttributes; - - private static readonly long PruningInterval = TimeSpan.FromSeconds(5).Ticks; - internal long _lastPruned = 0; - private readonly Lazy _realHub; /// /// Constructs a . @@ -40,14 +33,6 @@ public SentrySpanProcessor(IHub hub) : this(hub, null) internal SentrySpanProcessor(IHub hub, IEnumerable? enrichers) { _hub = hub; - _realHub = new Lazy(() => - _hub switch - { - Hub thisHub => thisHub, - HubAdapter when SentrySdk.CurrentHub is Hub sdkHub => sdkHub, - _ => null - }); - if (_hub is DisabledHub) { // This would only happen if someone tried to create a SentrySpanProcessor manually @@ -56,9 +41,7 @@ internal SentrySpanProcessor(IHub hub, IEnumerable? enri "You should use the TracerProviderBuilderExtensions to configure Sentry with OpenTelemetry"); } - _enrichers = enrichers ?? Enumerable.Empty(); _options = hub.GetSentryOptions(); - if (_options is null) { throw new InvalidOperationException( @@ -73,380 +56,34 @@ internal SentrySpanProcessor(IHub hub, IEnumerable? enri "to initialize the Sentry SDK with options.UseOpenTelemetry()"); } - // Resource attributes are consistent between spans, but not available during construction. - // Thus, get a single instance lazily. - _resourceAttributes = new Lazy>(() => - ParentProvider?.GetResource().Attributes.ToDict() ?? new Dictionary(0)); + _enrichers = enrichers ?? Enumerable.Empty(); + _activitySpanProcessor = new ActivitySpanProcessor(hub, ApplyEnrichers, GetResourceAttributes); } + internal ISpan? GetMappedSpan(ActivitySpanId spanId) => _activitySpanProcessor.GetMappedSpan(spanId); + /// public override void OnStart(Activity data) { - if (data.ParentSpanId != default && _map.TryGetValue(data.ParentSpanId, out var parentSpan)) - { - // We can find the parent span - start a child span. - var context = new SpanContext( - data.OperationName, - data.SpanId.AsSentrySpanId(), - data.ParentSpanId.AsSentrySpanId(), - data.TraceId.AsSentryId(), - data.DisplayName, - null, - null) - { - Instrumenter = Instrumenter.OpenTelemetry - }; - - var span = (SpanTracer)parentSpan.StartChild(context); - span.StartTimestamp = data.StartTimeUtc; - // Used to filter out spans that are not recorded when finishing a transaction. - span.SetFused(data); - span.IsFiltered = () => span.GetFused() is { IsAllDataRequested: false, Recorded: false }; - _map[data.SpanId] = span; - } - else - { - // If a parent exists at all, then copy its sampling decision. - bool? isSampled = data.HasRemoteParent ? data.Recorded : null; - - // No parent span found - start a new transaction - var transactionContext = new TransactionContext(data.DisplayName, - data.OperationName, - data.SpanId.AsSentrySpanId(), - data.ParentSpanId.AsSentrySpanId(), - data.TraceId.AsSentryId(), - data.DisplayName, null, isSampled, isSampled) - { - Instrumenter = Instrumenter.OpenTelemetry - }; - - var baggageHeader = data.Baggage.AsBaggageHeader(); - var dynamicSamplingContext = baggageHeader.CreateDynamicSamplingContext(); - var transaction = (TransactionTracer)_hub.StartTransaction( - transactionContext, new Dictionary(), dynamicSamplingContext - ); - transaction.StartTimestamp = data.StartTimeUtc; - _hub.ConfigureScope(scope => scope.Transaction = transaction); - transaction.SetFused(data); - _map[data.SpanId] = transaction; - } - - // Housekeeping - PruneFilteredSpans(); + _activitySpanProcessor.OnStart(data); } /// public override void OnEnd(Activity data) { - // Make a dictionary of the attributes (aka "tags") for faster lookup when used throughout the processor. - var attributes = data.TagObjects.ToDict(); - - var url = - attributes.TryGetTypedValue(OtelSemanticConventions.AttributeUrlFull, out string? tempUrl) ? tempUrl - : attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpUrl, out string? fallbackUrl) ? fallbackUrl // Falling back to pre-1.5.0 - : null; - - if (!string.IsNullOrEmpty(url) && (_options?.IsSentryRequest(url) ?? false)) - { - _options?.DiagnosticLogger?.LogDebug($"Ignoring Activity {data.SpanId} for Sentry request."); - - if (_map.TryRemove(data.SpanId, out var removed)) - { - if (removed is SpanTracer spanTracerToRemove) - { - spanTracerToRemove.IsSentryRequest = true; - } - - if (removed is TransactionTracer transactionTracer) - { - transactionTracer.IsSentryRequest = true; - } - } - - return; - } - - if (!_map.TryGetValue(data.SpanId, out var span)) - { - _options?.DiagnosticLogger?.LogError($"Span not found for SpanId: {data.SpanId}. Did OnStart run? We might have a bug in the SDK."); - return; - } - - var (operation, description, source) = ParseOtelSpanDescription(data, attributes); - span.Operation = operation; - span.Description = description; - - if (span is TransactionTracer transaction) - { - transaction.Name = description; - transaction.NameSource = source; - - // Use the end timestamp from the activity data. - transaction.EndTimestamp = data.StartTimeUtc + data.Duration; - - // Transactions set otel attributes (and resource attributes) as context. - transaction.Contexts["otel"] = GetOtelContext(attributes); - } - else - { - // Use the end timestamp from the activity data. - ((SpanTracer)span).EndTimestamp = data.StartTimeUtc + data.Duration; - - // Spans set otel attributes in extras (passed to Sentry as "data" on the span). - // Resource attributes do not need to be set, as they would be identical as those set on the transaction. - span.SetExtras(attributes); - span.SetExtra("otel.kind", data.Kind); - } - - // In ASP.NET Core the middleware finishes up (and the scope gets popped) before the activity is ended. So we - // need to restore the scope here (it's saved by our middleware when the request starts) - var activityScope = GetSavedScope(data); - if (activityScope is { } savedScope) - { - var hub = _realHub.Value; - hub?.RestoreScope(savedScope); - } - GenerateSentryErrorsFromOtelSpan(data, attributes); - - var status = GetSpanStatus(data.Status, attributes); - foreach (var enricher in _enrichers) - { - enricher.Enrich(span, data, _hub, _options); - } - span.Finish(status); - - _map.TryRemove(data.SpanId, out _); - - // Housekeeping - PruneFilteredSpans(); - } - - /// - /// Clean up items that may have been filtered out. - /// See https://github.com/getsentry/sentry-dotnet/pull/3198 - /// - internal void PruneFilteredSpans(bool force = false) - { - if (!force && !NeedsPruning()) - { - return; - } - - foreach (var mappedItem in _map) - { - var (spanId, span) = mappedItem; - var activity = span.GetFused(); - if (activity is { Recorded: false, IsAllDataRequested: false }) - { - _map.TryRemove(spanId, out _); - } - } - } - - private bool NeedsPruning() - { - var lastPruned = Interlocked.Read(ref _lastPruned); - if (lastPruned > DateTime.UtcNow.Ticks - PruningInterval) - { - return false; - } - - var thisPruned = DateTime.UtcNow.Ticks; - Interlocked.CompareExchange(ref _lastPruned, thisPruned, lastPruned); - // May be false if another thread gets there first - return Interlocked.Read(ref _lastPruned) == thisPruned; - } - - private static Scope? GetSavedScope(Activity? activity) - { - while (activity is not null) - { - if (activity.GetFused() is { } savedScope) - { - return savedScope; - } - activity = activity.Parent; - } - return null; - } - - internal static SpanStatus GetSpanStatus(ActivityStatusCode status, IDictionary attributes) - { - // See https://github.com/open-telemetry/opentelemetry-dotnet/discussions/4703 - if (attributes.TryGetValue(OtelSpanAttributeConstants.StatusCodeKey, out var statusCode) - && statusCode is OtelStatusTags.ErrorStatusCodeTagValue - ) - { - return GetErrorSpanStatus(attributes); - } - return status switch - { - ActivityStatusCode.Unset => SpanStatus.Ok, - ActivityStatusCode.Ok => SpanStatus.Ok, - ActivityStatusCode.Error => GetErrorSpanStatus(attributes), - _ => SpanStatus.UnknownError - }; - } - - private static SpanStatus GetErrorSpanStatus(IDictionary attributes) - { - if (attributes.TryGetTypedValue("http.status_code", out int httpCode)) - { - return SpanStatusConverter.FromHttpStatusCode(httpCode); - } - - if (attributes.TryGetTypedValue("rpc.grpc.status_code", out int grpcCode)) - { - return SpanStatusConverter.FromGrpcStatusCode(grpcCode); - } - - return SpanStatus.UnknownError; - } - - private static (string operation, string description, TransactionNameSource source) ParseOtelSpanDescription( - Activity activity, - IDictionary attributes) - { - // This function should loosely match the JavaScript implementation at: - // https://github.com/getsentry/sentry-javascript/blob/3487fa3af7aa72ac7fdb0439047cb7367c591e77/packages/opentelemetry-node/src/utils/parseOtelSpanDescription.ts - // However, it should also follow the OpenTelemetry semantic conventions specification, as indicated. - - // HTTP span - // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/http/ - if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpMethod, out string httpMethod)) - { - if (activity.Kind == ActivityKind.Client) - { - // Per OpenTelemetry spec, client spans use only the method. - return ("http.client", httpMethod, TransactionNameSource.Custom); - } - - if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpRoute, out string httpRoute)) - { - // A route exists. Use the method and route. - return ("http.server", $"{httpMethod} {httpRoute}", TransactionNameSource.Route); - } - - if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeHttpTarget, out string httpTarget)) - { - // A target exists. Use the method and target. If the target is "/" we can treat it like a route. - var source = httpTarget == "/" ? TransactionNameSource.Route : TransactionNameSource.Url; - return ("http.server", $"{httpMethod} {httpTarget}", source); - } - - // Some other type of HTTP server span. Pass it through with the original name. - return ("http.server", activity.DisplayName, TransactionNameSource.Custom); - } - - // DB span - // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/database/ - if (attributes.ContainsKey(OtelSemanticConventions.AttributeDbSystem)) - { - if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeDbStatement, out string dbStatement)) - { - // We have a database statement. Use it. - return ("db", dbStatement, TransactionNameSource.Task); - } - - // Some other type of DB span. Pass it through with the original name. - return ("db", activity.DisplayName, TransactionNameSource.Task); - } - - // RPC span - // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/rpc/ - if (attributes.ContainsKey(OtelSemanticConventions.AttributeRpcService)) - { - return ("rpc", activity.DisplayName, TransactionNameSource.Route); - } - - // Messaging span - // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/messaging/ - if (attributes.ContainsKey(OtelSemanticConventions.AttributeMessagingSystem)) - { - return ("message", activity.DisplayName, TransactionNameSource.Route); - } - - // FaaS (Functions/Lambda) span - // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/faas/ - if (attributes.TryGetTypedValue(OtelSemanticConventions.AttributeFaasTrigger, out string faasTrigger)) - { - return (faasTrigger, activity.DisplayName, TransactionNameSource.Route); - } - - // Default - pass through unmodified. - return (activity.OperationName, activity.DisplayName, TransactionNameSource.Custom); + _activitySpanProcessor.OnEnd(data); } - private Dictionary GetOtelContext(IDictionary attributes) + private void ApplyEnrichers(ISpan span, Activity data) { - var otelContext = new Dictionary(); - if (attributes.Count > 0) - { - otelContext.Add("attributes", attributes); - } - - var resourceAttributes = _resourceAttributes.Value; - if (resourceAttributes.Count > 0) + foreach (var enricher in _enrichers) { - otelContext.Add("resource", resourceAttributes); + enricher.Enrich(span, data, _hub, _options); } - - return otelContext; } - private void GenerateSentryErrorsFromOtelSpan(Activity activity, IDictionary spanAttributes) + private Dictionary GetResourceAttributes() { - // https://develop.sentry.dev/sdk/performance/opentelemetry/#step-7-define-generatesentryerrorsfromotelspan - // https://opentelemetry.io/docs/specs/otel/trace/semantic_conventions/exceptions/ - foreach (var @event in activity.Events.Where(e => e.Name == OtelSemanticConventions.AttributeExceptionEventName)) - { - var eventAttributes = @event.Tags.ToDict(); - // This would be where we would ideally implement full exception capture. That's not possible at the - // moment since the full exception isn't yet available via the OpenTelemetry API. - // See https://github.com/open-telemetry/opentelemetry-dotnet/issues/2439#issuecomment-1577314568 - // if (!eventAttributes.TryGetTypedValue("exception", out Exception exception)) - // { - // continue; - // } - - // At the moment, OTEL only gives us `exception.type`, `exception.message`, and `exception.stacktrace`... - // So the best we can do is a poor man's exception (no accurate symbolication or anything) - if (!eventAttributes.TryGetTypedValue(OtelSemanticConventions.AttributeExceptionType, out string exceptionType)) - { - continue; - } - eventAttributes.TryGetTypedValue(OtelSemanticConventions.AttributeExceptionMessage, out string message); - eventAttributes.TryGetTypedValue(OtelSemanticConventions.AttributeExceptionStacktrace, out string stackTrace); - - Exception exception; - try - { - var type = Type.GetType(exceptionType)!; - exception = (Exception)Activator.CreateInstance(type, message)!; - exception.SetSentryMechanism("SentrySpanProcessor.ErrorSpan"); - } - catch - { - _options?.DiagnosticLogger?.LogError($"Failed to create poor man's exception for type : {exceptionType}"); - continue; - } - - // TODO: Validate that our `DuplicateEventDetectionEventProcessor` prevents this from doubling exceptions - // that are also caught by other means, such as our AspNetCore middleware, etc. - // (When options.RecordException = true is set on AddAspNetCoreInstrumentation...) - // Also, in such cases - how will we get the otel scope and trace context on the other one? - - var sentryEvent = new SentryEvent(exception, @event.Timestamp); - var otelContext = GetOtelContext(spanAttributes); - otelContext.Add("stack_trace", stackTrace); - sentryEvent.Contexts["otel"] = otelContext; - _hub.CaptureEvent(sentryEvent, scope => - { - var trace = scope.Contexts.Trace; - trace.SpanId = activity.SpanId.AsSentrySpanId(); - trace.ParentSpanId = activity.ParentSpanId.AsSentrySpanId(); - trace.TraceId = activity.TraceId.AsSentryId(); - }); - } + return ParentProvider?.GetResource().Attributes.ToDict() ?? new Dictionary(0); } } diff --git a/src/Sentry/DefaultIntegrationsExtensions.cs b/src/Sentry/DefaultIntegrationsExtensions.cs new file mode 100644 index 0000000000..e9ddca023d --- /dev/null +++ b/src/Sentry/DefaultIntegrationsExtensions.cs @@ -0,0 +1,7 @@ +namespace Sentry; + +internal static class DefaultIntegrationsExtensions +{ + public static bool Includes(this SentryOptions.DefaultIntegrations integrations, SentryOptions.DefaultIntegrations value) + => (integrations & value) != 0; +} diff --git a/src/Sentry/HubExtensions.cs b/src/Sentry/HubExtensions.cs index a4bedfddec..7735c9bb6b 100644 --- a/src/Sentry/HubExtensions.cs +++ b/src/Sentry/HubExtensions.cs @@ -11,6 +11,18 @@ namespace Sentry; [EditorBrowsable(EditorBrowsableState.Never)] public static class HubExtensions { + /// + /// Starts a child span for the current transaction or, if there is no active transaction, starts a new transaction. + /// + internal static ISpan StartSpan(this IHub hub, string operation, string description) + { + ITransactionTracer? currentTransaction = null; + hub.ConfigureScope(s => currentTransaction = s.Transaction); + return currentTransaction is { } transaction + ? transaction.StartChild(operation, description) + : hub.StartTransaction(operation, description); + } + /// /// Starts a transaction. /// diff --git a/src/Sentry/IMetricHub.cs b/src/Sentry/IMetricHub.cs index 99f9d7d778..c486e8cb52 100644 --- a/src/Sentry/IMetricHub.cs +++ b/src/Sentry/IMetricHub.cs @@ -14,9 +14,7 @@ internal interface IMetricHub /// void CaptureCodeLocations(CodeLocations codeLocations); - /// - /// Starts a child span for the current transaction or, if there is no active transaction, starts a new transaction. - /// + /// ISpan StartSpan(string operation, string description); /// diff --git a/src/Sentry/Instrumenter.cs b/src/Sentry/Instrumenter.cs index f169216255..cdf3bfe575 100644 --- a/src/Sentry/Instrumenter.cs +++ b/src/Sentry/Instrumenter.cs @@ -10,6 +10,11 @@ public enum Instrumenter /// Sentry, + /// + /// Spans are instrumented with Sentry using ActivitySource. + /// + ActivitySource, + /// /// Spans are instrumented via OpenTelemetry. /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 1a82aa4fa9..7f08c51ba6 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,5 +1,6 @@ using Sentry.Extensibility; using Sentry.Infrastructure; +using Sentry.Internal.Tracing; using Sentry.Protocol.Envelopes; using Sentry.Protocol.Metrics; @@ -20,7 +21,7 @@ internal class Hub : IHub, IMetricHub, IDisposable // Internal for testability internal ConditionalWeakTable ExceptionToSpanMap { get; } = new(); - internal IInternalScopeManager ScopeManager { get; } + public IInternalScopeManager ScopeManager { get; } /// public IMetricAggregator Metrics { get; } @@ -75,6 +76,9 @@ internal Hub( options.LogDebug("Registering integration: '{0}'.", integration.GetType().Name); integration.Register(this, options); } + + // If no tracing integration was registered, fall back to Sentry tracing + options.InternalTraceProvider ??= new SentryTraceProvider(this); } public void ConfigureScope(Action configureScope) @@ -119,16 +123,6 @@ internal ITransactionTracer StartTransaction( IReadOnlyDictionary customSamplingContext, DynamicSamplingContext? dynamicSamplingContext) { - var instrumenter = (context as SpanContext)?.Instrumenter; - if (instrumenter != _options.Instrumenter) - { - _options.LogWarning( - $"Attempted to start a transaction via {instrumenter} instrumentation when the SDK is" + - $" configured for {_options.Instrumenter} instrumentation. The transaction will not be created."); - - return NoOpTransaction.Instance; - } - var transaction = new TransactionTracer(this, context); // If the hub is disabled, we will always sample out. In other words, starting a transaction @@ -539,14 +533,10 @@ public void CaptureCodeLocations(CodeLocations codeLocations) } } - /// + /// public ISpan StartSpan(string operation, string description) { - ITransactionTracer? currentTransaction = null; - ConfigureScope(s => currentTransaction = s.Transaction); - return currentTransaction is { } transaction - ? transaction.StartChild(operation, description) - : this.StartTransaction(operation, description); + return HubExtensions.StartSpan(this, operation, description); } public void CaptureSession(SessionUpdate sessionUpdate) diff --git a/src/Sentry/Internal/Tracing/ITraceProvider.cs b/src/Sentry/Internal/Tracing/ITraceProvider.cs new file mode 100644 index 0000000000..f1bd67a537 --- /dev/null +++ b/src/Sentry/Internal/Tracing/ITraceProvider.cs @@ -0,0 +1,6 @@ +namespace Sentry.Internal.Tracing; + +internal interface ITraceProvider +{ + public ITracer GetTracer(string name, string? version = ""); +} diff --git a/src/Sentry/Internal/Tracing/ITraceSpan.cs b/src/Sentry/Internal/Tracing/ITraceSpan.cs new file mode 100644 index 0000000000..de72b6b636 --- /dev/null +++ b/src/Sentry/Internal/Tracing/ITraceSpan.cs @@ -0,0 +1,28 @@ +namespace Sentry.Internal.Tracing; + +internal interface ITraceSpan : IDisposable +{ + string? Description { get; } + ITraceSpan AddEvent(string message); + ITraceSpan SetAttribute(string key, object value); + ITraceSpan SetDescription(string? description); + ITraceSpan SetStatus(SpanStatus status, string? description = default); + ITraceSpan Stop(); + + ITraceSpan SetExtra(string key, object? value); + + ITraceSpan Finish(Exception exception); +} + +internal static class TraceSpanExtensions +{ + internal static ITraceSpan SetExtras(this ITraceSpan traceSpan, IEnumerable> values) + { + foreach (var (key, value) in values) + { + traceSpan.SetExtra(key, value); + } + + return traceSpan; + } +} diff --git a/src/Sentry/Internal/Tracing/ITracer.cs b/src/Sentry/Internal/Tracing/ITracer.cs new file mode 100644 index 0000000000..006023b02b --- /dev/null +++ b/src/Sentry/Internal/Tracing/ITracer.cs @@ -0,0 +1,7 @@ +namespace Sentry.Internal.Tracing; + +internal interface ITracer +{ + ITraceSpan? StartSpan(string operationName, string? description = null); + ITraceSpan? CurrentSpan { get; } +} diff --git a/src/Sentry/Internal/Tracing/SentryTraceProvider.cs b/src/Sentry/Internal/Tracing/SentryTraceProvider.cs new file mode 100644 index 0000000000..ee02f1807a --- /dev/null +++ b/src/Sentry/Internal/Tracing/SentryTraceProvider.cs @@ -0,0 +1,11 @@ +namespace Sentry.Internal.Tracing; + +internal class SentryTraceProvider(IHub hub) : ITraceProvider +{ + private readonly SentryTracer _tracer = new(hub); + + // Sentry doesn't have the same concept of "Tracers" as the DiagnosticSource classes do, so we always return the + // same tracer for Sentry... it's just a wrapper around the Hub which is actually what Starts and Stops spans when + // using Sentry tracing. + public ITracer GetTracer(string name, string? version = "") => _tracer; +} diff --git a/src/Sentry/Internal/Tracing/SentryTraceSpan.cs b/src/Sentry/Internal/Tracing/SentryTraceSpan.cs new file mode 100644 index 0000000000..d00c48c6d4 --- /dev/null +++ b/src/Sentry/Internal/Tracing/SentryTraceSpan.cs @@ -0,0 +1,74 @@ +namespace Sentry.Internal.Tracing; + +internal class SentryTraceSpan : ITraceSpan +{ + private readonly ISpan _span; + private Scope? _scope; + + public string? Description => _span.Description; + + public SentryTraceSpan(IHub hub, ISpan span) + { + _span = span; + hub.ConfigureScope(scope => _scope = scope); + } + + public void Dispose() + { + // ISpan doesn't implement IDisposable + } + + public ITraceSpan AddEvent(string message) + { + _scope?.AddBreadcrumb(message); + return this; + } + + public ITraceSpan SetAttribute(string key, object value) + { + var stringValue = $"{value}"; + if (string.IsNullOrWhiteSpace(stringValue)) + { + _span.UnsetTag(key); + } + else + { + _span.SetTag(key, stringValue); + } + return this; + } + + public ITraceSpan SetDescription(string? description) + { + _span.Description = description; + return this; + } + + public ITraceSpan SetStatus(SpanStatus status, string? description = default) + { + _span.Status = status; + if (_span.Status != SpanStatus.Ok) + { + _span.Description = description; + } + return this; + } + + public ITraceSpan Stop() + { + _span.Finish(); + return this; + } + + public ITraceSpan SetExtra(string key, object? value) + { + _span.SetExtra(key, value); + return this; + } + + public ITraceSpan Finish(Exception exception) + { + _span.Finish(exception); + return this; + } +} diff --git a/src/Sentry/Internal/Tracing/SentryTracer.cs b/src/Sentry/Internal/Tracing/SentryTracer.cs new file mode 100644 index 0000000000..5d9209db4f --- /dev/null +++ b/src/Sentry/Internal/Tracing/SentryTracer.cs @@ -0,0 +1,13 @@ +namespace Sentry.Internal.Tracing; + +internal class SentryTracer(IHub hub) : ITracer +{ + public ITraceSpan? StartSpan(string operationName, string? description = null) => new SentryTraceSpan( + hub, + hub.StartSpan(operationName, description ?? operationName) + ); + + public ITraceSpan? CurrentSpan => hub.GetSpan() is { } span + ? new SentryTraceSpan(hub, span) + : null; +} diff --git a/src/Sentry/Internal/Tracing/SentryTracingIntegration.cs b/src/Sentry/Internal/Tracing/SentryTracingIntegration.cs new file mode 100644 index 0000000000..28b51beb16 --- /dev/null +++ b/src/Sentry/Internal/Tracing/SentryTracingIntegration.cs @@ -0,0 +1,19 @@ +using Sentry.Extensibility; +using Sentry.Integrations; + +namespace Sentry.Internal.Tracing; + +internal class SentryTracingIntegration : ISdkIntegration +{ + public void Register(IHub hub, SentryOptions options) + { + if (!options.IsPerformanceMonitoringEnabled) + { + options.Log(SentryLevel.Info, "SentryTracing Integration is disabled because tracing is disabled."); + return; + } + + // TODO: Should we be registering this if OpenTelemetry is enabled? + options.InternalTraceProvider = new SentryTraceProvider(hub); + } +} diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj index 77d1d4b965..f08e8c4dc8 100644 --- a/src/Sentry/Sentry.csproj +++ b/src/Sentry/Sentry.csproj @@ -52,12 +52,19 @@ $(DefineConstants);HAS_DIAGNOSTIC_INTEGRATION + $(DefineConstants);HAS_ACTIVITY_TRACING_INTEGRATION - - Internal\%(RecursiveDir)%(Filename)%(Extension) + + Internal\DiagnosticSource\%(RecursiveDir)%(Filename)%(Extension) + + + Internal\Tracing\%(RecursiveDir)%(Filename)%(Extension) + + $(DefineConstants);HAS_DIAGNOSTICS_7_OR_GREATER + - - - - - - - - + diff --git a/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs b/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs index 693619aece..940b405319 100644 --- a/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/SentryPropagatorTests.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Primitives; using OpenTelemetry; using OpenTelemetry.Context.Propagation; +using Sentry.Internal.Tracing; namespace Sentry.OpenTelemetry.Tests; diff --git a/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs b/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs index aab7671d1b..52f58ff113 100644 --- a/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs +++ b/test/Sentry.OpenTelemetry.Tests/SentrySpanProcessorTests.cs @@ -1,4 +1,5 @@ using Sentry.Internal.OpenTelemetry; +using Sentry.Internal.Tracing; namespace Sentry.OpenTelemetry.Tests; @@ -65,247 +66,6 @@ public void Ctor_Instrumenter_Not_OpenTelemetry_Throws() Assert.Throws(() => _fixture.GetSut()); } - [Fact] - public void GetSpanStatus() - { - using (new AssertionScope()) - { - var noAttributes = new Dictionary(); - - // Unset and OK -> OK - SentrySpanProcessor.GetSpanStatus(ActivityStatusCode.Unset, noAttributes).Should().Be(SpanStatus.Ok); - SentrySpanProcessor.GetSpanStatus(ActivityStatusCode.Ok, noAttributes).Should().Be(SpanStatus.Ok); - - // Error (no attributes) -> UnknownError - SentrySpanProcessor.GetSpanStatus(ActivityStatusCode.Error, noAttributes) - .Should().Be(SpanStatus.UnknownError); - - // Unknown status code -> UnknownError - SentrySpanProcessor.GetSpanStatus((ActivityStatusCode)42, noAttributes) - .Should().Be(SpanStatus.UnknownError); - - // We only test one http scenario, just to make sure the SpanStatusConverter is called for these headers. - // Tests for SpanStatusConverter ensure other http status codes would also work though - var notFoundAttributes = new Dictionary { ["http.status_code"] = 404 }; - SentrySpanProcessor.GetSpanStatus(ActivityStatusCode.Error, notFoundAttributes) - .Should().Be(SpanStatus.NotFound); - - // We only test one grpc scenario, just to make sure the SpanStatusConverter is called for these headers. - // Tests for SpanStatusConverter ensure other grpc status codes would also work though - var grpcAttributes = new Dictionary { ["rpc.grpc.status_code"] = 7 }; - SentrySpanProcessor.GetSpanStatus(ActivityStatusCode.Error, grpcAttributes) - .Should().Be(SpanStatus.PermissionDenied); - - var errorAttributes = new Dictionary { [OtelSpanAttributeConstants.StatusCodeKey] = OtelStatusTags.ErrorStatusCodeTagValue }; - SentrySpanProcessor.GetSpanStatus(ActivityStatusCode.Ok, errorAttributes).Should().Be(SpanStatus.UnknownError); - SentrySpanProcessor.GetSpanStatus(ActivityStatusCode.Unset, errorAttributes).Should().Be(SpanStatus.UnknownError); - } - } - - [Fact] - public void OnStart_Transaction_With_DynamicSamplingContext() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - var expected = new Dictionary() - { - { "trace_id", SentryId.Create().ToString() }, - { "public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" }, - { "sample_rate", "0.5" }, - }; - var data = Tracer.StartActivity("test op")!; - data.AddBaggage($"{BaggageHeader.SentryKeyPrefix}trace_id", expected["trace_id"]); - data.AddBaggage($"{BaggageHeader.SentryKeyPrefix}public_key", expected["public_key"]); - data.AddBaggage($"{BaggageHeader.SentryKeyPrefix}sample_rate", expected["sample_rate"]); - - // Act - sut.OnStart(data!); - - // Assert - Assert.True(sut._map.TryGetValue(data.SpanId, out var span)); - if (span is not TransactionTracer transaction) - { - Assert.Fail("Span is not a transaction tracer"); - return; - } - if (transaction.DynamicSamplingContext is not { } actual) - { - Assert.Fail("Transaction does not have a dynamic sampling context"); - return; - } - using (new AssertionScope()) - { - actual.Items["trace_id"].Should().Be(expected["trace_id"]); - actual.Items["public_key"].Should().Be(expected["public_key"]); - actual.Items["sample_rate"].Should().Be(expected["sample_rate"]); - } - } - - [Fact] - public void OnStart_WithParentSpanId_StartsChildSpan() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - using var parent = Tracer.StartActivity("Parent"); - sut.OnStart(parent); - - using var data = Tracer.StartActivity("TestActivity"); - - // Act - sut.OnStart(data!); - - // Assert - Assert.True(sut._map.TryGetValue(data.SpanId, out var span)); - using (new AssertionScope()) - { - span.Should().BeOfType(); - span.SpanId.Should().Be(data.SpanId.AsSentrySpanId()); - span.ParentSpanId.Should().Be(data.ParentSpanId.AsSentrySpanId()); - if (span is not SpanTracer spanTracer) - { - Assert.Fail("Span is not a span tracer"); - return; - } - using (new AssertionScope()) - { - spanTracer.SpanId.Should().Be(data.SpanId.AsSentrySpanId()); - spanTracer.ParentSpanId.Should().Be(data.ParentSpanId.AsSentrySpanId()); - spanTracer.TraceId.Should().Be(data.TraceId.AsSentryId()); - spanTracer.Operation.Should().Be(data.OperationName); - spanTracer.Description.Should().Be(data.DisplayName); - spanTracer.Status.Should().BeNull(); - spanTracer.StartTimestamp.Should().Be(data.StartTimeUtc); - } - } - } - - [Fact] - public void OnStart_WithoutParentSpanId_StartsNewTransaction() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - _fixture.ScopeManager = Substitute.For(); - var sut = _fixture.GetSut(); - - var data = Tracer.StartActivity("test op"); - - // Act - sut.OnStart(data!); - - // Assert - Assert.True(sut._map.TryGetValue(data.SpanId, out var span)); - if (span is not TransactionTracer transaction) - { - Assert.Fail("Span is not a transaction tracer"); - return; - } - using (new AssertionScope()) - { - transaction.SpanId.Should().Be(data.SpanId.AsSentrySpanId()); - transaction.ParentSpanId.Should().Be(new ActivitySpanId().AsSentrySpanId()); - transaction.TraceId.Should().Be(data.TraceId.AsSentryId()); - transaction.Name.Should().Be(data.DisplayName); - transaction.Operation.Should().Be(data.OperationName); - transaction.Description.Should().Be(data.DisplayName); - transaction.Status.Should().BeNull(); - transaction.StartTimestamp.Should().Be(data.StartTimeUtc); - _fixture.ScopeManager.Received(1).ConfigureScope(Arg.Any>()); - } - } - - [Fact] - public void OnEnd_FinishesSpan() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - var parent = Tracer.StartActivity(name: "transaction")!; - sut.OnStart(parent); - - var tags = new Dictionary { - { "foo", "bar" } - }; - var data = Tracer.StartActivity(name: "test operation", kind: ActivityKind.Internal, parentContext: default, tags)!; - data.DisplayName = "test display name"; - sut.OnStart(data); - - sut._map.TryGetValue(data.SpanId, out var span); - - // Act - sut.OnEnd(data); - - // Assert - if (span is not SpanTracer spanTracer) - { - Assert.Fail("Span is not a span tracer"); - return; - } - - using (new AssertionScope()) - { - spanTracer.ParentSpanId.Should().Be(parent.SpanId.AsSentrySpanId()); - spanTracer.Operation.Should().Be(data.OperationName); - spanTracer.Description.Should().Be(data.DisplayName); - spanTracer.EndTimestamp.Should().NotBeNull(); - spanTracer.Extra["otel.kind"].Should().Be(data.Kind); - foreach (var keyValuePair in tags) - { - span.Extra[keyValuePair.Key].Should().Be(keyValuePair.Value); - } - - spanTracer.Status.Should().Be(SpanStatus.Ok); - } - } - - [Fact] - public void OnEnd_Transaction_RestoresSavedScope() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - _fixture.ScopeManager = Substitute.For(); - var sut = _fixture.GetSut(); - - var scope = new Scope(); - var data = Tracer.StartActivity("transaction")!; - data.SetFused(scope); - sut.OnStart(data); - - // Act - sut.OnEnd(data); - - // Assert - _fixture.ScopeManager.Received(1).RestoreScope(scope); - } - - [Fact] - public void OnEnd_Span_RestoresSavedScope() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - _fixture.ScopeManager = Substitute.For(); - var sut = _fixture.GetSut(); - - var scope = new Scope(); - var parent = Tracer.StartActivity("transaction")!; - parent.SetFused(scope); - sut.OnStart(parent); - - var data = Tracer.StartActivity("test operation")!; - data.DisplayName = "test display name"; - sut.OnStart(data); - - // Act - sut.OnEnd(data); - - // Assert - _fixture.ScopeManager.Received(1).RestoreScope(scope); - } - [Fact] public void OnEnd_SpansEnriched() { @@ -319,7 +79,7 @@ public void OnEnd_SpansEnriched() var parent = Tracer.StartActivity(name: "transaction")!; sut.OnStart(parent); - sut._map.TryGetValue(parent.SpanId, out var span); + var span = sut.GetMappedSpan(parent.SpanId); // Act sut.OnEnd(parent); @@ -334,172 +94,4 @@ public void OnEnd_SpansEnriched() transactionTracer.Tags.TryGetValue("foo", out var foo).Should().BeTrue(); foo.Should().Be("bar"); } - - [Fact] - public void OnEnd_FinishesTransaction() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - var tags = new Dictionary { - { "foo", "bar" } - }; - var data = Tracer.StartActivity(name: "test operation", kind: ActivityKind.Internal, parentContext: default, tags)!; - data.DisplayName = "test display name"; - sut.OnStart(data); - - sut._map.TryGetValue(data.SpanId, out var span); - - // Act - sut.OnEnd(data); - - // Assert - if (span is not TransactionTracer transaction) - { - Assert.Fail("Span is not a transaction tracer"); - return; - } - - using (new AssertionScope()) - { - transaction.ParentSpanId.Should().Be(new ActivitySpanId().AsSentrySpanId()); - transaction.Operation.Should().Be(data.OperationName); - transaction.Description.Should().Be(data.DisplayName); - transaction.Name.Should().Be(data.DisplayName); - transaction.NameSource.Should().Be(TransactionNameSource.Custom); - transaction.EndTimestamp.Should().NotBeNull(); - transaction.Contexts["otel"].Should().BeEquivalentTo(new Dictionary - { - { "attributes", tags } - }); - transaction.Status.Should().Be(SpanStatus.Ok); - } - } - - [Theory] - [InlineData(OtelSemanticConventions.AttributeUrlFull)] - [InlineData(OtelSemanticConventions.AttributeHttpUrl)] - public void OnEnd_IsSentryRequest_DoesNotFinishTransaction(string urlKey) - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - var tags = new Dictionary { { "foo", "bar" }, { urlKey, _fixture.Options.Dsn } }; - var data = Tracer.StartActivity(name: "test operation", kind: ActivityKind.Internal, parentContext: default, tags)!; - data.DisplayName = "test display name"; - sut.OnStart(data); - - sut._map.TryGetValue(data.SpanId, out var span); - - // Act - sut.OnEnd(data); - - // Assert - if (span is not TransactionTracer transaction) - { - Assert.Fail("Span is not a transaction tracer"); - return; - } - - transaction.IsSentryRequest.Should().BeTrue(); - } - - private static void FilterActivity(Activity activity) - { - // Simulates filtering an activity - see https://github.com/getsentry/sentry-dotnet/pull/3198 - activity.IsAllDataRequested = false; - activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; - } - - [Fact] - public void PruneFilteredSpans_FilteredTransactions_Pruned() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - using var parent = Tracer.StartActivity(); - sut.OnStart(parent!); - - FilterActivity(parent); - - // Act - sut.PruneFilteredSpans(true); - - // Assert - Assert.False(sut._map.TryGetValue(parent.SpanId, out var _)); - } - - [Fact] - public void PruneFilteredSpans_UnFilteredTransactions_NotPruned() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - using var parent = Tracer.StartActivity(); - sut.OnStart(parent!); - - // Act - sut.PruneFilteredSpans(true); - - // Assert - Assert.True(sut._map.TryGetValue(parent.SpanId, out var _)); - } - - [Fact] - public void PruneFilteredSpans_FilteredSpans_Pruned() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - using var parent = Tracer.StartActivity(); - sut.OnStart(parent!); - - using var activity1 = Tracer.StartActivity(); - sut.OnStart(activity1!); - - using var activity2 = Tracer.StartActivity(); - sut.OnStart(activity2!); - - FilterActivity(activity2); - - // Act - sut.PruneFilteredSpans(true); - - // Assert - Assert.True(sut._map.TryGetValue(activity1.SpanId, out var _)); - Assert.False(sut._map.TryGetValue(activity2.SpanId, out var _)); - } - - [Fact] - public void PruneFilteredSpans_RecentlyPruned_DoesNothing() - { - // Arrange - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var sut = _fixture.GetSut(); - - sut._lastPruned = DateTimeOffset.MaxValue.Ticks; // fake a recent prune - - using var parent = Tracer.StartActivity(); - sut.OnStart(parent!); - - using var activity1 = Tracer.StartActivity(); - sut.OnStart(activity1!); - - using var activity2 = Tracer.StartActivity(); - sut.OnStart(activity2!); - - FilterActivity(activity2); - - // Act - sut.PruneFilteredSpans(); - - // Assert - Assert.True(sut._map.TryGetValue(activity1.SpanId, out var _)); - Assert.True(sut._map.TryGetValue(activity2.SpanId, out var _)); - } } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index cca662c335..1af3c4e205 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -317,7 +317,8 @@ namespace Sentry public enum Instrumenter { Sentry = 0, - OpenTelemetry = 1, + ActivitySource = 1, + OpenTelemetry = 2, } public readonly struct MeasurementUnit : System.IEquatable { @@ -545,7 +546,9 @@ namespace Sentry public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler { public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public class SentryHint @@ -564,7 +567,9 @@ namespace Sentry public SentryHttpMessageHandler(Sentry.IHub hub) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method will be removed in future versions.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public readonly struct SentryId : Sentry.ISentryJsonSerializable, System.IEquatable @@ -613,7 +618,9 @@ namespace Sentry protected SentryMessageHandler(Sentry.IHub hub) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected abstract void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url); + [System.Obsolete("This method will be removed in future versions.")] protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } @@ -698,6 +705,7 @@ namespace Sentry public void AddTransactionProcessorProvider(System.Func> processorProvider) { } public void AddTransactionProcessors(System.Collections.Generic.IEnumerable processors) { } public void ApplyDefaultTags(Sentry.IHasTags hasTags) { } + public void DisableActivityTracingIntegration() { } public void DisableAppDomainProcessExitFlush() { } public void DisableAppDomainUnhandledExceptionCapture() { } public void DisableDiagnosticSourceIntegration() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index cca662c335..1af3c4e205 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -317,7 +317,8 @@ namespace Sentry public enum Instrumenter { Sentry = 0, - OpenTelemetry = 1, + ActivitySource = 1, + OpenTelemetry = 2, } public readonly struct MeasurementUnit : System.IEquatable { @@ -545,7 +546,9 @@ namespace Sentry public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler { public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public class SentryHint @@ -564,7 +567,9 @@ namespace Sentry public SentryHttpMessageHandler(Sentry.IHub hub) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method will be removed in future versions.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public readonly struct SentryId : Sentry.ISentryJsonSerializable, System.IEquatable @@ -613,7 +618,9 @@ namespace Sentry protected SentryMessageHandler(Sentry.IHub hub) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected abstract void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url); + [System.Obsolete("This method will be removed in future versions.")] protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } @@ -698,6 +705,7 @@ namespace Sentry public void AddTransactionProcessorProvider(System.Func> processorProvider) { } public void AddTransactionProcessors(System.Collections.Generic.IEnumerable processors) { } public void ApplyDefaultTags(Sentry.IHasTags hasTags) { } + public void DisableActivityTracingIntegration() { } public void DisableAppDomainProcessExitFlush() { } public void DisableAppDomainUnhandledExceptionCapture() { } public void DisableDiagnosticSourceIntegration() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index d61bb3c82d..763c645d5e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -318,7 +318,8 @@ namespace Sentry public enum Instrumenter { Sentry = 0, - OpenTelemetry = 1, + ActivitySource = 1, + OpenTelemetry = 2, } public readonly struct MeasurementUnit : System.IEquatable { @@ -546,7 +547,9 @@ namespace Sentry public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler { public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public class SentryHint @@ -565,7 +568,9 @@ namespace Sentry public SentryHttpMessageHandler(Sentry.IHub hub) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method will be removed in future versions.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public readonly struct SentryId : Sentry.ISentryJsonSerializable, System.IEquatable @@ -614,7 +619,9 @@ namespace Sentry protected SentryMessageHandler(Sentry.IHub hub) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected abstract void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url); + [System.Obsolete("This method will be removed in future versions.")] protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); protected override System.Net.Http.HttpResponseMessage Send(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } @@ -699,6 +706,7 @@ namespace Sentry public void AddTransactionProcessorProvider(System.Func> processorProvider) { } public void AddTransactionProcessors(System.Collections.Generic.IEnumerable processors) { } public void ApplyDefaultTags(Sentry.IHasTags hasTags) { } + public void DisableActivityTracingIntegration() { } public void DisableAppDomainProcessExitFlush() { } public void DisableAppDomainUnhandledExceptionCapture() { } public void DisableDiagnosticSourceIntegration() { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 0ea27a9ec7..37b2261a6d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -316,7 +316,8 @@ namespace Sentry public enum Instrumenter { Sentry = 0, - OpenTelemetry = 1, + ActivitySource = 1, + OpenTelemetry = 2, } public readonly struct MeasurementUnit : System.IEquatable { @@ -544,7 +545,9 @@ namespace Sentry public class SentryGraphQLHttpMessageHandler : Sentry.SentryMessageHandler { public SentryGraphQLHttpMessageHandler(System.Net.Http.HttpMessageHandler? innerHandler = null, Sentry.IHub? hub = null) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method is obsolete and will be removed in a future version.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public class SentryHint @@ -563,7 +566,9 @@ namespace Sentry public SentryHttpMessageHandler(Sentry.IHub hub) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } public SentryHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected override void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url) { } + [System.Obsolete("This method will be removed in future versions.")] protected override Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url) { } } public readonly struct SentryId : Sentry.ISentryJsonSerializable, System.IEquatable @@ -612,7 +617,9 @@ namespace Sentry protected SentryMessageHandler(Sentry.IHub hub) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { } protected SentryMessageHandler(System.Net.Http.HttpMessageHandler innerHandler, Sentry.IHub hub) { } + [System.Obsolete("This method will be removed in future versions.")] protected abstract void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.ISpan? span, string method, string url); + [System.Obsolete("This method will be removed in future versions.")] protected abstract Sentry.ISpan? ProcessRequest(System.Net.Http.HttpRequestMessage request, string method, string url); protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { } } diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index e99a9e304e..64a5988f3d 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -616,46 +616,6 @@ public void StartTransaction_DisableTracing_SampledOut() transaction.IsSampled.Should().BeFalse(); } - [Fact] - public void StartTransaction_SameInstrumenter_SampledIn() - { - // Arrange - _fixture.Options.EnableTracing = true; - _fixture.Options.Instrumenter = Instrumenter.Sentry; // The default... making it explicit for this test though - var hub = _fixture.GetSut(); - - var transactionContext = new TransactionContext("name", "operation") - { - Instrumenter = _fixture.Options.Instrumenter - }; - - // Act - var transaction = hub.StartTransaction(transactionContext); - - // Assert - transaction.IsSampled.Should().BeTrue(); - } - - [Fact] - public void StartTransaction_DifferentInstrumenter_NoOp() - { - // Arrange - _fixture.Options.EnableTracing = true; - _fixture.Options.Instrumenter = Instrumenter.OpenTelemetry; - var hub = _fixture.GetSut(); - - var transactionContext = new TransactionContext("name", "operation") - { - Instrumenter = Instrumenter.Sentry // The default... making it explicit for this test though - }; - - // Act - var transaction = hub.StartTransaction(transactionContext); - - // Assert - transaction.Should().Be(NoOpTransaction.Instance); - } - [Fact] public void StartTransaction_EnableTracing_Sampler_SampledIn() { diff --git a/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs index 4aa1ac21f0..513d04f018 100644 --- a/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryGraphQlHttpMessageHandlerTests.cs @@ -23,6 +23,7 @@ public class SentryGraphQlHttpMessageHandlerTests private StringContent ValidResponseContent => SentryGraphQlTestHelpers.ResponesContent(ValidResponse); [Fact] + [Obsolete("Obsolete")] public void ProcessRequest_ExtractsGraphQlRequestContent() { // Arrange @@ -44,6 +45,7 @@ public void ProcessRequest_ExtractsGraphQlRequestContent() } [Fact] + [Obsolete("Obsolete")] public void ProcessRequest_SetsSpanData() { // Arrange @@ -74,6 +76,7 @@ public void ProcessRequest_SetsSpanData() // [Theory] // [InlineData(ValidQuery)] [Fact] + [Obsolete("Obsolete")] public void HandleResponse_AddsBreadcrumb() { // Arrange @@ -115,6 +118,7 @@ public void HandleResponse_AddsBreadcrumb() } [Fact] + [Obsolete("Obsolete")] public void HandleResponse_SetsSpanData() { // Arrange diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index fc38202019..744e18cef5 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -240,6 +240,7 @@ public async Task SendAsync_Executed_FailedRequestsCaptured() } [Fact] + [Obsolete("Obsolete")] public void ProcessRequest_SetsSpanData() { // Arrange @@ -267,6 +268,7 @@ public void ProcessRequest_SetsSpanData() } [Fact] + [Obsolete("Obsolete")] public void HandleResponse_SetsSpanData() { // Arrange diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt index dfbc55fc32..3d12fd4b6b 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.DotNet.verified.txt @@ -34,5 +34,11 @@ Args: [ SentryDiagnosticListenerIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + ActivityTracingIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt index 10eaeaf749..44ec09a1a1 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet6_0.Windows.DotNet.verified.txt @@ -40,5 +40,11 @@ Args: [ WinUIUnhandledExceptionIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + ActivityTracingIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt index dfbc55fc32..3d12fd4b6b 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.DotNet.verified.txt @@ -34,5 +34,11 @@ Args: [ SentryDiagnosticListenerIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + ActivityTracingIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt index 10eaeaf749..44ec09a1a1 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet7_0.Windows.DotNet.verified.txt @@ -40,5 +40,11 @@ Args: [ WinUIUnhandledExceptionIntegration ] + }, + { + Message: Registering integration: '{0}'., + Args: [ + ActivityTracingIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt index 9e3f2681b5..3bb7b3eecf 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.DotNet.verified.txt @@ -44,5 +44,11 @@ { Level: info, Message: System.Diagnostics.Metrics Integration is disabled because no listeners are configured. + }, + { + Message: Registering integration: '{0}'., + Args: [ + ActivityTracingIntegration + ] } ] \ No newline at end of file diff --git a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt index e14cfaf075..25a5ea7a08 100644 --- a/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt +++ b/test/Sentry.Tests/SentryOptionsTests.Integrations_default_ones_are_properly_registered.DotNet8_0.Windows.DotNet.verified.txt @@ -50,5 +50,11 @@ { Level: info, Message: System.Diagnostics.Metrics Integration is disabled because no listeners are configured. + }, + { + Message: Registering integration: '{0}'., + Args: [ + ActivityTracingIntegration + ] } ] \ No newline at end of file