-
-
Notifications
You must be signed in to change notification settings - Fork 241
feat: add IHub.RecordTransaction to record completed transactions and spans #5333
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1a688ee
feat: add IHub.RecordTransaction to record completed transactions and…
jamescrosswell 055094b
Format code
getsentry-bot 835ea90
refactor: address review — merge into HubExtensions, add SentrySdk fa…
jamescrosswell ea3494f
Merge remote-tracking branch 'origin/feat/2683-record-transaction-spa…
jamescrosswell 3b71273
refactor: collapse recorders onto a shared SpanRecorderBase
jamescrosswell File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| using Sentry.Internal; | ||
|
|
||
| namespace Sentry; | ||
|
|
||
| public static partial class HubExtensions | ||
| { | ||
| /// <summary> | ||
| /// Records a transaction that has already completed elsewhere — for example, spans measured on another | ||
| /// machine or process and replayed through a proxy. Timing is supplied explicitly rather than measured | ||
| /// live, so unlike <see cref="StartTransaction(IHub, string, string)"/> there is no stopwatch, idle timer, | ||
| /// or sampling decision involved; the transaction is materialized and captured once when | ||
| /// <paramref name="configure"/> returns. | ||
| /// </summary> | ||
| /// <param name="hub">The hub.</param> | ||
| /// <param name="name">The transaction name.</param> | ||
| /// <param name="operation">The transaction operation.</param> | ||
| /// <param name="startTimestamp">When the transaction started.</param> | ||
| /// <param name="duration">How long the transaction ran. Must not be negative.</param> | ||
| /// <param name="traceId">Optional trace id to preserve from the originating system; generated when omitted.</param> | ||
| /// <param name="spanId">Optional root span id to preserve; generated when omitted.</param> | ||
| /// <param name="parentSpanId">Optional parent span id, when continuing a trace from another service.</param> | ||
| /// <param name="configure"> | ||
| /// Optional callback to set metadata and record the span tree via | ||
| /// <see cref="ISpanRecorder.RecordSpan(string, DateTimeOffset, TimeSpan, SpanId?, Action{ISpanRecorder}?)"/>. | ||
| /// </param> | ||
| /// <returns>The event id of the captured transaction.</returns> | ||
| public static SentryId RecordTransaction( | ||
|
jamescrosswell marked this conversation as resolved.
Outdated
|
||
| this IHub hub, | ||
| string name, | ||
| string operation, | ||
| DateTimeOffset startTimestamp, | ||
| TimeSpan duration, | ||
| SentryId? traceId = null, | ||
| SpanId? spanId = null, | ||
| SpanId? parentSpanId = null, | ||
| Action<ITransactionRecorder>? configure = null) | ||
| { | ||
| if (duration < TimeSpan.Zero) | ||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(duration), duration, "Transaction duration cannot be negative."); | ||
| } | ||
|
|
||
| var context = new TransactionContext( | ||
| name, | ||
| operation, | ||
| spanId: spanId, | ||
| parentSpanId: parentSpanId, | ||
| traceId: traceId, | ||
| isSampled: true); | ||
|
|
||
| var tracer = new TransactionTracer(hub, context) | ||
| { | ||
| StartTimestamp = startTimestamp, | ||
| EndTimestamp = startTimestamp + duration, | ||
| }; | ||
| tracer.Status ??= SpanStatus.Ok; | ||
|
|
||
| var recorder = new TransactionRecorder(tracer); | ||
| configure?.Invoke(recorder); | ||
|
|
||
| var transaction = new SentryTransaction(tracer); | ||
|
|
||
| // A recorded transaction represents work that happened elsewhere, so we don't want the current | ||
| // (live) scope's breadcrumbs/user/tags/contexts leaking onto it. Capture against a clean scope when | ||
| // we have options to construct one; otherwise fall back (e.g. a disabled hub, where this is a no-op). | ||
| var options = hub.GetSentryOptions(); | ||
| if (options is not null) | ||
| { | ||
| hub.CaptureTransaction(transaction, new Scope(options), null); | ||
|
jamescrosswell marked this conversation as resolved.
Outdated
|
||
| } | ||
| else | ||
| { | ||
| hub.CaptureTransaction(transaction); | ||
| } | ||
|
|
||
| return transaction.EventId; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| namespace Sentry; | ||
|
|
||
| /// <summary> | ||
| /// Builds a span while recording a transaction that has already completed elsewhere (for example, work | ||
| /// measured on another machine and replayed through a proxy). Unlike <see cref="ISpan"/>, timing is supplied | ||
| /// up-front rather than measured live, so a recorded span can never be left half-specified. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Obtained from <see cref="ITransactionRecorder"/> or a parent <see cref="ISpanRecorder"/> via | ||
| /// <see cref="RecordSpan(string, DateTimeOffset, TimeSpan, SpanId?, Action{ISpanRecorder}?)"/>. | ||
| /// See <see cref="HubExtensions.RecordTransaction"/>. | ||
| /// </remarks> | ||
| public interface ISpanRecorder | ||
| { | ||
| /// <summary> | ||
| /// The span's id. When not overridden at creation, one is generated. | ||
| /// </summary> | ||
| SpanId SpanId { get; } | ||
|
|
||
| /// <summary> | ||
| /// Span description. | ||
| /// </summary> | ||
| string? Description { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Span status. Defaults to <see cref="SpanStatus.Ok"/> when not set. | ||
| /// </summary> | ||
| SpanStatus? Status { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Sets a tag on the span. | ||
| /// </summary> | ||
| void SetTag(string key, string value); | ||
|
|
||
| /// <summary> | ||
| /// Sets arbitrary data on the span. | ||
| /// </summary> | ||
| void SetData(string key, object? value); | ||
|
|
||
| /// <summary> | ||
| /// Records a child span nested under this one. The parent is structural (this span), so no parent id | ||
| /// needs to be supplied. Pass <paramref name="spanId"/> to preserve an id from the originating system. | ||
| /// </summary> | ||
| /// <param name="operation">The span operation.</param> | ||
| /// <param name="startTimestamp">When the child span started.</param> | ||
| /// <param name="duration">How long the child span ran. Must not be negative.</param> | ||
| /// <param name="spanId">Optional span id to preserve; generated when omitted.</param> | ||
| /// <param name="configure">Optional callback to set metadata and record further nested spans.</param> | ||
| /// <returns>The recorder for the child span.</returns> | ||
| ISpanRecorder RecordSpan( | ||
| string operation, | ||
| DateTimeOffset startTimestamp, | ||
| TimeSpan duration, | ||
| SpanId? spanId = null, | ||
| Action<ISpanRecorder>? configure = null); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Builds a transaction (and its span tree) that has already completed elsewhere. Obtained inside the | ||
| /// <c>configure</c> callback of <see cref="HubExtensions.RecordTransaction"/>. When the callback returns, the | ||
| /// whole tree is materialized and captured once — no live tracing, stopwatch, or sampling roll is involved. | ||
| /// </summary> | ||
| public interface ITransactionRecorder : ISpanRecorder | ||
|
jamescrosswell marked this conversation as resolved.
|
||
| { | ||
| /// <summary> | ||
| /// The trace this transaction belongs to. Set at creation (via <see cref="HubExtensions.RecordTransaction"/>) | ||
| /// and inherited by every recorded span. | ||
| /// </summary> | ||
| SentryId TraceId { get; } | ||
|
|
||
| /// <summary> | ||
| /// The release that produced this transaction. Useful when the origin system differs from this process. | ||
| /// </summary> | ||
| string? Release { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The environment the transaction ran in. Useful when the origin system differs from this process. | ||
| /// </summary> | ||
| string? Environment { get; set; } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| namespace Sentry.Internal; | ||
|
|
||
| /// <summary> | ||
| /// Shared logic for materializing a recorded child span onto an existing <see cref="TransactionTracer"/>. | ||
| /// </summary> | ||
| internal static class SpanRecorderInternals | ||
|
jamescrosswell marked this conversation as resolved.
Outdated
|
||
| { | ||
| public static SpanRecorder RecordChild( | ||
| TransactionTracer owner, | ||
| SpanId parentSpanId, | ||
| string operation, | ||
| DateTimeOffset startTimestamp, | ||
| TimeSpan duration, | ||
| SpanId? spanId, | ||
| Action<ISpanRecorder>? configure) | ||
| { | ||
| if (duration < TimeSpan.Zero) | ||
| { | ||
| throw new ArgumentOutOfRangeException(nameof(duration), duration, "Span duration cannot be negative."); | ||
| } | ||
|
|
||
| // Reuse the existing child-span machinery so the span is added to the transaction's flat span | ||
| // collection with the correct parent pointer; then override the timing the tracer would otherwise | ||
| // have measured with a stopwatch. | ||
| var span = (SpanTracer)owner.StartChild(spanId, parentSpanId, operation); | ||
| span.StartTimestamp = startTimestamp; | ||
| span.EndTimestamp = startTimestamp + duration; | ||
| span.Status ??= SpanStatus.Ok; | ||
|
|
||
| var recorder = new SpanRecorder(owner, span); | ||
| configure?.Invoke(recorder); | ||
| return recorder; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// <see cref="ISpanRecorder"/> backed by a <see cref="SpanTracer"/> whose timing has been set explicitly. | ||
| /// </summary> | ||
| internal sealed class SpanRecorder : ISpanRecorder | ||
| { | ||
| private readonly TransactionTracer _owner; | ||
| private readonly SpanTracer _span; | ||
|
|
||
| public SpanRecorder(TransactionTracer owner, SpanTracer span) | ||
| { | ||
| _owner = owner; | ||
| _span = span; | ||
| } | ||
|
|
||
| public SpanId SpanId => _span.SpanId; | ||
|
|
||
| public string? Description | ||
| { | ||
| get => _span.Description; | ||
| set => _span.Description = value; | ||
| } | ||
|
|
||
| public SpanStatus? Status | ||
| { | ||
| get => _span.Status; | ||
| set => _span.Status = value; | ||
| } | ||
|
|
||
| public void SetTag(string key, string value) => _span.SetTag(key, value); | ||
|
|
||
| public void SetData(string key, object? value) => _span.SetData(key, value); | ||
|
|
||
| public ISpanRecorder RecordSpan( | ||
| string operation, | ||
| DateTimeOffset startTimestamp, | ||
| TimeSpan duration, | ||
| SpanId? spanId = null, | ||
| Action<ISpanRecorder>? configure = null) => | ||
| SpanRecorderInternals.RecordChild(_owner, _span.SpanId, operation, startTimestamp, duration, spanId, configure); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// <see cref="ITransactionRecorder"/> backed by a <see cref="TransactionTracer"/> whose timing has been set | ||
| /// explicitly. The tracer is never <c>Finish()</c>-ed (which would reset the live scope); instead the caller | ||
| /// converts it to a <see cref="SentryTransaction"/> and captures it directly. | ||
| /// </summary> | ||
| internal sealed class TransactionRecorder : ITransactionRecorder | ||
| { | ||
| private readonly TransactionTracer _tracer; | ||
|
|
||
| public TransactionRecorder(TransactionTracer tracer) | ||
| { | ||
| _tracer = tracer; | ||
| } | ||
|
|
||
| internal TransactionTracer Tracer => _tracer; | ||
|
|
||
| public SpanId SpanId => _tracer.SpanId; | ||
|
|
||
| public SentryId TraceId => _tracer.TraceId; | ||
|
|
||
| public string? Description | ||
| { | ||
| get => _tracer.Description; | ||
| set => _tracer.Description = value; | ||
| } | ||
|
|
||
| public SpanStatus? Status | ||
| { | ||
| get => _tracer.Status; | ||
| set => _tracer.Status = value; | ||
| } | ||
|
|
||
| public string? Release | ||
| { | ||
| get => _tracer.Release; | ||
| set => _tracer.Release = value; | ||
| } | ||
|
|
||
| public string? Environment | ||
| { | ||
| get => _tracer.Environment; | ||
| set => _tracer.Environment = value; | ||
| } | ||
|
|
||
| public void SetTag(string key, string value) => _tracer.SetTag(key, value); | ||
|
|
||
| public void SetData(string key, object? value) => _tracer.SetData(key, value); | ||
|
|
||
| public ISpanRecorder RecordSpan( | ||
| string operation, | ||
| DateTimeOffset startTimestamp, | ||
| TimeSpan duration, | ||
| SpanId? spanId = null, | ||
| Action<ISpanRecorder>? configure = null) => | ||
| SpanRecorderInternals.RecordChild(_tracer, _tracer.SpanId, operation, startTimestamp, duration, spanId, configure); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.