Skip to content
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

Integrate trace headers #758

Merged
merged 16 commits into from
Jan 26, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## vNext

- Add support for dynamic transaction sampling. (#753) @Tyrrrz
- Integrate trace headers. (#758) @Tyrrrz
- Renamed Option `DiagnosticsLevel` to `DiagnosticLevel` (#759) @bruno-garcia

## 3.0.0-beta.0
Expand Down
4 changes: 2 additions & 2 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public ITransaction StartTransaction(
/// <summary>
/// Returns null.
/// </summary>
public SentryTraceHeader? GetSentryTrace() => null;
public SentryTraceHeader? GetTraceHeader() => null;

/// <summary>
/// No-Op.
Expand All @@ -82,7 +82,7 @@ public void BindClient(ISentryClient client)
/// <summary>
/// No-Op.
/// </summary>
public void CaptureTransaction(Transaction transaction)
public void CaptureTransaction(ITransaction transaction)
{
}

Expand Down
4 changes: 2 additions & 2 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public ITransaction StartTransaction(
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
public SentryTraceHeader? GetSentryTrace()
public SentryTraceHeader? GetTraceHeader()
=> SentrySdk.GetTraceHeader();

/// <summary>
Expand Down Expand Up @@ -153,7 +153,7 @@ public SentryId CaptureEvent(SentryEvent evt, Scope? scope)
/// </summary>
[DebuggerStepThrough]
[EditorBrowsable(EditorBrowsableState.Never)]
public void CaptureTransaction(Transaction transaction)
public void CaptureTransaction(ITransaction transaction)
=> SentrySdk.CaptureTransaction(transaction);

/// <summary>
Expand Down
21 changes: 21 additions & 0 deletions src/Sentry/HubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ public static ITransaction StartTransaction(
return transaction;
}

/// <summary>
/// Starts a transaction from the specified trace header.
/// </summary>
public static ITransaction StartTransaction(
this IHub hub,
string name,
string operation,
SentryTraceHeader traceHeader)
{
var context = new TransactionContext(
// SpanId from the header becomes ParentSpanId on this transaction
traceHeader.SpanId,
traceHeader.TraceId,
name,
operation,
traceHeader.IsSampled
);

return hub.StartTransaction(context);
}

/// <summary>
/// Adds a breadcrumb to the current scope.
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Sentry/IHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ ITransaction StartTransaction(
);

/// <summary>
/// Gets the sentry trace header.
/// Gets the Sentry trace header for the last active span.
/// </summary>
SentryTraceHeader? GetSentryTrace();
SentryTraceHeader? GetTraceHeader();
}
}
6 changes: 5 additions & 1 deletion src/Sentry/ISentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ public interface ISentryClient
/// <summary>
/// Captures a transaction.
/// </summary>
/// <remarks>
/// Note: this method is NOT meant to be called from user code!
/// Instead, call <see cref="ISpan.Finish"/> on the transaction.
/// </remarks>
/// <param name="transaction">The transaction.</param>
void CaptureTransaction(Transaction transaction);
void CaptureTransaction(ITransaction transaction);

/// <summary>
/// Flushes events queued up.
Expand Down
15 changes: 12 additions & 3 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ public ITransaction StartTransaction(
return transaction;
}

public SentryTraceHeader? GetSentryTrace()
public SentryTraceHeader? GetTraceHeader()
{
var (currentScope, _) = ScopeManager.GetCurrent();
return currentScope.Transaction?.GetTraceHeader();
return currentScope.GetSpan()?.GetTraceHeader();
}

public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null)
Expand Down Expand Up @@ -191,11 +191,20 @@ public void CaptureUserFeedback(UserFeedback userFeedback)
}
}

public void CaptureTransaction(Transaction transaction)
public void CaptureTransaction(ITransaction transaction)
{
try
{
_ownedClient.CaptureTransaction(transaction);

// Clear the transaction from the scope
ScopeManager.WithScope(scope =>
{
if (scope.Transaction == transaction)
{
scope.Transaction = null;
}
});
}
catch (Exception e)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Protocol/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback)
/// <summary>
/// Creates an envelope that contains a single transaction.
/// </summary>
public static Envelope FromTransaction(Transaction transaction)
public static Envelope FromTransaction(ITransaction transaction)
{
var header = new Dictionary<string, object?>(StringComparer.Ordinal)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,14 @@ public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback)
/// <summary>
/// Creates an envelope item from transaction.
/// </summary>
public static EnvelopeItem FromTransaction(Transaction transaction)
public static EnvelopeItem FromTransaction(ITransaction transaction)
{
var header = new Dictionary<string, object?>(StringComparer.Ordinal)
{
[TypeKey] = TypeValueTransaction
};

return new EnvelopeItem(header, new JsonSerializable(transaction));
return new EnvelopeItem(header, new JsonSerializable((IJsonSerializable)transaction));
}

/// <summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry/Protocol/IJsonSerializable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ internal interface IJsonSerializable
/// <summary>
/// Writes the object as JSON.
/// </summary>
/// <remarks>
/// Note: this method is meant only for internal use and is exposed due to a language limitation.
/// Avoid relying on this method in user code.
/// </remarks>
void WriteTo(Utf8JsonWriter writer);
}

Expand Down
10 changes: 10 additions & 0 deletions src/Sentry/Protocol/ISpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public interface ISpan : ISpanContext, IHasTags, IHasExtra
/// </summary>
DateTimeOffset? EndTimestamp { get; }

/// <summary>
/// Whether the span is finished.
/// </summary>
bool IsFinished { get; }

/// <summary>
/// Starts a child span.
/// </summary>
Expand All @@ -44,6 +49,11 @@ public interface ISpan : ISpanContext, IHasTags, IHasExtra
/// Finishes the span.
/// </summary>
void Finish();

/// <summary>
/// Get Sentry trace header.
/// </summary>
SentryTraceHeader GetTraceHeader();
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Sentry/Protocol/ITransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public interface ITransaction : ISpan, ITransactionContext, IEventLike
IReadOnlyCollection<ISpan> Spans { get; }

/// <summary>
/// Get Sentry trace header.
/// Gets the last active (not finished) span in this transaction.
/// </summary>
SentryTraceHeader GetTraceHeader();
ISpan? GetLastActiveSpan();
}
}
33 changes: 23 additions & 10 deletions src/Sentry/Protocol/SentryTraceHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,37 @@ namespace Sentry.Protocol
/// </summary>
public class SentryTraceHeader
{
private readonly SentryId _traceId;
private readonly SpanId _spanId;
private readonly bool? _isSampled;
internal const string HttpHeaderName = "sentry-trace";

/// <summary>
/// Trace ID.
/// </summary>
public SentryId TraceId { get; }

/// <summary>
/// Span ID.
/// </summary>
public SpanId SpanId { get; }

/// <summary>
/// Whether the trace is sampled.
/// </summary>
public bool? IsSampled { get; }

/// <summary>
/// Initializes an instance of <see cref="SentryTraceHeader"/>.
/// </summary>
public SentryTraceHeader(SentryId traceId, SpanId spanId, bool? isSampled)
public SentryTraceHeader(SentryId traceId, SpanId spanSpanId, bool? isSampled)
{
_traceId = traceId;
_spanId = spanId;
_isSampled = isSampled;
TraceId = traceId;
SpanId = spanSpanId;
IsSampled = isSampled;
}

/// <inheritdoc />
public override string ToString() => _isSampled is {} isSampled
? $"{_traceId}-{_spanId}-{(isSampled ? 1 : 0)}"
: $"{_traceId}-{_spanId}";
public override string ToString() => IsSampled is {} isSampled
? $"{TraceId}-{SpanId}-{(isSampled ? 1 : 0)}"
: $"{TraceId}-{SpanId}";

/// <summary>
/// Parses <see cref="SentryTraceHeader"/> from string.
Expand Down
10 changes: 10 additions & 0 deletions src/Sentry/Protocol/Span.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public class Span : ISpan, IJsonSerializable
/// <inheritdoc />
public DateTimeOffset? EndTimestamp { get; private set; }

/// <inheritdoc />
public bool IsFinished => EndTimestamp is not null;

/// <inheritdoc cref="ISpan.Operation" />
public string Operation { get; set; }

Expand Down Expand Up @@ -86,6 +89,13 @@ public ISpan StartChild(string operation) =>
/// <inheritdoc />
public void Finish() => EndTimestamp = DateTimeOffset.UtcNow;

/// <inheritdoc />
public SentryTraceHeader GetTraceHeader() => new(
TraceId,
SpanId,
IsSampled
);

/// <inheritdoc />
public void WriteTo(Utf8JsonWriter writer)
{
Expand Down
10 changes: 8 additions & 2 deletions src/Sentry/Protocol/Transaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@ public SentryId TraceId
public string Name { get; set; }

/// <inheritdoc />
public DateTimeOffset StartTimestamp { get; private set; } = DateTimeOffset.UtcNow;
public DateTimeOffset StartTimestamp { get; internal set; } = DateTimeOffset.UtcNow;

/// <inheritdoc />
public DateTimeOffset? EndTimestamp { get; private set; }
public DateTimeOffset? EndTimestamp { get; internal set; }

/// <inheritdoc cref="ISpan.Operation" />
public string Operation
Expand Down Expand Up @@ -170,6 +170,9 @@ public IReadOnlyList<string> Fingerprint
/// <inheritdoc />
public IReadOnlyCollection<ISpan> Spans => _spansLazy.Value;

/// <inheritdoc />
public bool IsFinished => EndTimestamp is not null;

// This constructor is used for deserialization purposes.
// It's required because some of the fields are mapped on 'contexts.trace'.
// When deserializing, we don't parse those fields explicitly, but
Expand Down Expand Up @@ -251,6 +254,9 @@ public void Finish()
_client.CaptureTransaction(this);
}

/// <inheritdoc />
public ISpan? GetLastActiveSpan() => Spans.LastOrDefault(s => !s.IsFinished);

/// <inheritdoc />
public SentryTraceHeader GetTraceHeader() => new(
TraceId,
Expand Down
7 changes: 4 additions & 3 deletions src/Sentry/Protocol/TransactionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ public TransactionContext(
/// Initializes an instance of <see cref="TransactionContext"/>.
/// </summary>
public TransactionContext(
SpanId? parentSpanId,
SentryId traceId,
string name,
string operation,
string description,
bool? isSampled)
: this(SpanId.Create(), null, SentryId.Create(), name, operation, description, null, isSampled)
: this(SpanId.Create(), parentSpanId, traceId, name, operation, "", null, isSampled)
{
}

Expand All @@ -44,7 +45,7 @@ public TransactionContext(
string name,
string operation,
bool? isSampled)
: this(name, operation, "", isSampled)
: this(null, SentryId.Create(), name, operation, isSampled)
{
}

Expand Down
6 changes: 6 additions & 0 deletions src/Sentry/Scope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,5 +382,11 @@ internal void Evaluate()
}
}
}

/// <summary>
/// Gets the currently ongoing (not finished) span or <code>null</code> if none available.
/// This relies on the transactions being manually set on the scope via <see cref="Transaction"/>.
/// </summary>
public ISpan? GetSpan() => Transaction?.GetLastActiveSpan() ?? Transaction;
}
}
19 changes: 17 additions & 2 deletions src/Sentry/SentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public void CaptureUserFeedback(UserFeedback userFeedback)
}

/// <inheritdoc />
public void CaptureTransaction(Transaction transaction)
public void CaptureTransaction(ITransaction transaction)
{
if (_disposed)
{
Expand All @@ -129,8 +129,23 @@ public void CaptureTransaction(Transaction transaction)
return;
}

// Unfinished transaction can only happen if the user calls this method instead of
// transaction.Finish().
// We still send these transactions over, but warn the user not to do it.
if (!transaction.IsFinished)
{
_options.DiagnosticLogger?.LogWarning(
"Capturing a transaction which has not been finished. " +
"Please call transaction.Finish() instead of hub.CaptureTransaction(transaction) " +
"to properly finalize the transaction and send it to Sentry."
);
}

// Sampling decision MUST have been made at this point
Debug.Assert(transaction.IsSampled != null, "Attempt to capture transaction without sampling decision.");
Debug.Assert(
transaction.IsSampled != null,
"Attempt to capture transaction without sampling decision."
);

if (transaction.IsSampled != true)
{
Expand Down
Loading