-
-
Notifications
You must be signed in to change notification settings - Fork 237
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
base: main
Are you sure you want to change the base?
Changes from all commits
1a688ee
055094b
835ea90
ea3494f
3b71273
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| 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> | ||
| public SpanId SpanId { get; } | ||
|
|
||
| /// <summary> | ||
| /// Span description. | ||
| /// </summary> | ||
| public string? Description { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Span status. Defaults to <see cref="SpanStatus.Ok"/> when not set. | ||
| /// </summary> | ||
| public SpanStatus? Status { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Sets a tag on the span. | ||
| /// </summary> | ||
| public void SetTag(string key, string value); | ||
|
|
||
| /// <summary> | ||
| /// Sets arbitrary data on the span. | ||
| /// </summary> | ||
| public 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> | ||
| public 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 | ||
| { | ||
| /// <summary> | ||
| /// The trace this transaction belongs to. Set at creation (via <see cref="HubExtensions.RecordTransaction"/>) | ||
| /// and inherited by every recorded span. | ||
| /// </summary> | ||
| public SentryId TraceId { get; } | ||
|
|
||
| /// <summary> | ||
| /// The release that produced this transaction. Useful when the origin system differs from this process. | ||
| /// </summary> | ||
| public string? Release { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// The environment the transaction ran in. Useful when the origin system differs from this process. | ||
| /// </summary> | ||
| public string? Environment { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Configures the (clean) scope the recorded transaction is captured against. Use this to set data that | ||
| /// was captured alongside the original trace — user, tags, contexts, breadcrumbs — without inheriting the | ||
| /// current process's live scope. | ||
| /// </summary> | ||
| public void ConfigureScope(Action<Scope> configureScope); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| namespace Sentry.Internal; | ||
|
|
||
| /// <summary> | ||
| /// Base <see cref="ISpanRecorder"/> for a node in a recorded transaction tree. Holds the owning | ||
| /// <see cref="TransactionTracer"/> (which new child spans are created on) and the node this recorder | ||
| /// represents (a <see cref="SpanTracer"/>, or the transaction tracer itself for the root), proxying metadata | ||
| /// straight through to it. | ||
| /// </summary> | ||
| internal abstract class SpanRecorderBase : ISpanRecorder | ||
| { | ||
| /// <summary>The transaction that owns the whole tree; child spans are created on it.</summary> | ||
| protected TransactionTracer Owner { get; } | ||
|
|
||
| private readonly ISpan _node; | ||
|
|
||
| protected SpanRecorderBase(TransactionTracer owner, ISpan node) | ||
| { | ||
| Owner = owner; | ||
| _node = node; | ||
| } | ||
|
|
||
| public SpanId SpanId => _node.SpanId; | ||
|
|
||
| public string? Description | ||
| { | ||
| get => _node.Description; | ||
| set => _node.Description = value; | ||
| } | ||
|
|
||
| public SpanStatus? Status | ||
| { | ||
| get => _node.Status; | ||
| set => _node.Status = value; | ||
| } | ||
|
|
||
| public void SetTag(string key, string value) => _node.SetTag(key, value); | ||
|
|
||
| public void SetData(string key, object? value) => _node.SetData(key, value); | ||
|
|
||
| public ISpanRecorder RecordSpan( | ||
| string operation, | ||
| DateTimeOffset startTimestamp, | ||
| TimeSpan duration, | ||
| SpanId? spanId = null, | ||
| Action<ISpanRecorder>? configure = null) | ||
| { | ||
| 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, SpanId, 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 : SpanRecorderBase | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: Arguably we don't need Probably the main reason to do this is so that we can add the |
||
| { | ||
| public SpanRecorder(TransactionTracer owner, SpanTracer span) | ||
| : base(owner, span) | ||
| { | ||
| } | ||
| } | ||
|
|
||
| /// <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 : SpanRecorderBase, ITransactionRecorder | ||
| { | ||
| private readonly Scope? _scope; | ||
|
|
||
| public TransactionRecorder(TransactionTracer tracer, Scope? scope) | ||
| : base(tracer, tracer) | ||
| { | ||
| _scope = scope; | ||
| } | ||
|
|
||
| public SentryId TraceId => Owner.TraceId; | ||
|
|
||
| public string? Release | ||
| { | ||
| get => Owner.Release; | ||
| set => Owner.Release = value; | ||
| } | ||
|
|
||
| public string? Environment | ||
| { | ||
| get => Owner.Environment; | ||
| set => Owner.Environment = value; | ||
| } | ||
|
|
||
| public void ConfigureScope(Action<Scope> configureScope) | ||
| { | ||
| // No scope when options are unavailable (e.g. a disabled hub); configuration is a no-op. | ||
| if (_scope is not null) | ||
| { | ||
| configureScope(_scope); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.