-
-
Notifications
You must be signed in to change notification settings - Fork 206
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
Support performance #633
Support performance #633
Changes from 5 commits
f31adac
79d2397
d7c9974
51783e8
c99635d
7ad5ab5
96c443e
98e567d
e0eb730
748670c
dfdec31
fa64053
8338381
8fc5c2b
6a05871
6c0b928
5a47bd9
e67597e
52ec0a5
15d7001
3a705f0
c4f9371
00ef2b5
b0487cf
0a713f3
c314a66
9e2b7e1
d0c5358
a4e01d8
d960d7f
a0fb93e
6c1b413
258ed5c
f2e5a8b
41bcb99
da7ee94
1371006
bafe82a
7abdb4f
a1e48f8
fd13150
134d0ec
6b92788
b6f3ee2
0e91066
ea06cfb
2cb49ef
e908cee
e8aa3cc
71c87ca
0c512b1
fbd6f17
13b88db
eb37814
61085a4
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 |
---|---|---|
|
@@ -104,7 +104,9 @@ public async Task InvokeAsync(HttpContext context) | |
// event creation will be sent to Sentry | ||
|
||
scope.OnEvaluating += (_, __) => PopulateScope(context, scope); | ||
scope.Transaction.StartTimestamp = DateTimeOffset.Now; | ||
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. Here we shoudl be doing: @HazAT @rhcarvalho @untitaker @maciejwalkowiak does the pseudo code below make sense?
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.
Other than that it does make sense. 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. If it can serve as inspiration, here's the equivalent middleware in Go: https://github.com/getsentry/sentry-go/blob/cca482cffbf600853b2f3abda730f9d5a97ee9aa/http/sentryhttp.go#L80-L101
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. One point to add:
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.
Is this documented somewhere? 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. Possibly not :/ |
||
}); | ||
|
||
try | ||
{ | ||
await _next(context).ConfigureAwait(false); | ||
|
@@ -122,6 +124,13 @@ public async Task InvokeAsync(HttpContext context) | |
|
||
ExceptionDispatchInfo.Capture(e).Throw(); | ||
} | ||
finally | ||
{ | ||
hub.ConfigureScope(scope => | ||
Tyrrrz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
scope.Transaction.Finish(); | ||
}); | ||
} | ||
|
||
void CaptureException(Exception e) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
using Sentry.Protocol; | ||
|
||
namespace Sentry.AspNetCore | ||
{ | ||
internal static class SpanStatusMapper | ||
{ | ||
public static SpanStatus FromStatusCode(int statusCode) => statusCode switch | ||
{ | ||
400 => SpanStatus.InvalidArgument, | ||
401 => SpanStatus.Unauthenticated, | ||
403 => SpanStatus.PermissionDenied, | ||
404 => SpanStatus.NotFound, | ||
409 => SpanStatus.AlreadyExists, | ||
429 => SpanStatus.ResourceExhausted, | ||
499 => SpanStatus.Cancelled, | ||
500 => SpanStatus.InternalError, | ||
501 => SpanStatus.Unimplemented, | ||
503 => SpanStatus.Unavailable, | ||
504 => SpanStatus.DeadlineExceeded, | ||
_ => SpanStatus.UnknownError | ||
Tyrrrz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace Sentry | ||
{ | ||
public interface ISentryTraceSampler | ||
{ | ||
double GetSampleRate(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
|
||
namespace Sentry.Protocol | ||
{ | ||
public interface ISpan | ||
{ | ||
SentryId SpanId { get; } | ||
|
||
SentryId? ParentSpanId { get; } | ||
|
||
SentryId TraceId { get; set; } | ||
|
||
DateTimeOffset StartTimestamp { get; set; } | ||
|
||
DateTimeOffset EndTimestamp { get; set; } | ||
Tyrrrz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
string? Operation { get; set; } | ||
Tyrrrz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
string? Description { get; set; } | ||
|
||
SpanStatus? Status { get; set; } | ||
|
||
bool IsSampled { get; set; } | ||
|
||
IReadOnlyDictionary<string, string> Tags { get; } | ||
|
||
IReadOnlyDictionary<string, object> Data { get; } | ||
|
||
ISpan StartChild(); | ||
|
||
void Finish(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text.Json; | ||
using Sentry.Internal.Extensions; | ||
|
||
namespace Sentry.Protocol | ||
{ | ||
// https://develop.sentry.dev/sdk/event-payloads/span | ||
public class Span : ISpan, IJsonSerializable | ||
{ | ||
public SentryId SpanId { get; } | ||
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. The protocol really has all of these members being mutable? 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. The protocol doesn't say anything about mutability. It doesn't even cover some of the fields at all. 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. We should address that. Have a discussion and write it on the docs. |
||
public SentryId? ParentSpanId { get; } | ||
public SentryId TraceId { get; set; } | ||
public DateTimeOffset StartTimestamp { get; set; } | ||
Tyrrrz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public DateTimeOffset EndTimestamp { get; set; } | ||
public string? Operation { get; set; } | ||
public string? Description { get; set; } | ||
public SpanStatus? Status { get; set; } | ||
public bool IsSampled { get; set; } | ||
|
||
private ConcurrentDictionary<string, string>? _tags; | ||
public IReadOnlyDictionary<string, string> Tags => _tags ??= new ConcurrentDictionary<string, string>(); | ||
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. I assume concurrency here might be more of an issue so this approach of instantiating on the getter won't work well here. We might need to synchronize in order to make sure concurrent calls to 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. So Spans have tags? I was thinking that only Transactions could have tags 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. Maybe just eagerly initialize the instance? It will probably be a smaller overhead than a lock. 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. one will incur overhead on each new span due to allocating the list then GC'ing it. The other one will cost when accessing it only and only the first time due to the double check lock. Without measuring we're just guessing, the the transaction I'm happy with always having the list there since more often than not we'll use it, for individual Spans, I don't know 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.
Maybe I misunderstood your suggestion, but how will that work? From my understanding, the lock has to be on the getter, which would mean it's acquired/checked every time the property is accessed. 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. 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. @lucas-zimerman the docs say so, but I also found the docs to be incredibly unreliable too, so... 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. If the docs are not good, we should fix it. Please open a PR there or at least raise a flag (open an issue asking for clarification). |
||
|
||
private ConcurrentDictionary<string, object>? _data; | ||
public IReadOnlyDictionary<string, object> Data => _data ??= new ConcurrentDictionary<string, object>(); | ||
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. Maybe Lazy with 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. Then we can't seed it with data during deserialization 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. Sorry, I don't follow, what do you mean? The current approach is racy though. As concurrent access to this will yield multiple threads creating their own collection and assigning to the field, which is not immediately visible to all threads because the field isn't The simplest is to just have the instance from the start of the object but since this field is likely not going to be used often, paying the price to allocating it on each span, which will have dozens of instances per transaction, we're better off just using lazy or other mechanism |
||
|
||
public Span(SentryId? spanId = null, SentryId? parentSpanId = null) | ||
{ | ||
SpanId = spanId ?? SentryId.Create(); | ||
ParentSpanId = parentSpanId; | ||
StartTimestamp = EndTimestamp = DateTimeOffset.Now; | ||
Tyrrrz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
public ISpan StartChild() => new Span(parentSpanId: SpanId); | ||
|
||
public void Finish() | ||
{ | ||
EndTimestamp = DateTimeOffset.Now; | ||
} | ||
|
||
public void WriteTo(Utf8JsonWriter writer) | ||
{ | ||
writer.WriteStartObject(); | ||
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. Odd that this is a mutable object that will be accessed concurrently and we're reading its state here to serialize. We need to make sure that all members we're accessing can be accessed concurrently. 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. Do you have a suggestion what we should do here? We could try "freezing" the span once it's finished, but it will be hard to guarantee that nothing gets written to the dictionaries. |
||
|
||
writer.WriteString("span_id", SpanId); | ||
|
||
if (ParentSpanId is {} parentSpanId) | ||
{ | ||
writer.WriteString("parent_span_id", parentSpanId); | ||
} | ||
|
||
writer.WriteString("trace_id", TraceId); | ||
writer.WriteString("start_timestamp", StartTimestamp); | ||
writer.WriteString("timestamp", EndTimestamp); | ||
|
||
if (!string.IsNullOrWhiteSpace(Operation)) | ||
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. This is mutable so it's racy. It could be non empty here but suddenly empty on the next line 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. do you think the span should be frozen after finishing? or when serializing? 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. It seems natural to me that once it's finished it should be frozen. Curious what other SDKs did here. |
||
{ | ||
writer.WriteString("op", Operation); | ||
} | ||
|
||
if (!string.IsNullOrWhiteSpace(Description)) | ||
{ | ||
writer.WriteString("description", Description); | ||
} | ||
|
||
if (Status is {} status) | ||
{ | ||
writer.WriteString("status", status.ToString().ToLowerInvariant()); | ||
} | ||
|
||
writer.WriteBoolean("sampled", IsSampled); | ||
|
||
if (_tags is {} tags && tags.Any()) | ||
{ | ||
writer.WriteDictionary("tags", tags!); | ||
} | ||
|
||
if (_data is {} data && data.Any()) | ||
{ | ||
writer.WriteDictionary("data", data!); | ||
} | ||
|
||
writer.WriteEndObject(); | ||
} | ||
|
||
public static Span FromJson(JsonElement json) | ||
{ | ||
var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; | ||
var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); | ||
var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; | ||
var startTimestamp = json.GetPropertyOrNull("start_timestamp")?.GetDateTimeOffset() ?? default; | ||
var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; | ||
var operation = json.GetPropertyOrNull("op")?.GetString(); | ||
var description = json.GetPropertyOrNull("description")?.GetString(); | ||
var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum<SpanStatus>()); | ||
var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; | ||
var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary<string, string>(v!)); | ||
var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary<string, object>(v!)); | ||
|
||
return new Span(spanId, parentSpanId) | ||
{ | ||
TraceId = traceId, | ||
StartTimestamp = startTimestamp, | ||
EndTimestamp = endTimestamp, | ||
Operation = operation, | ||
Description = description, | ||
Status = status, | ||
IsSampled = sampled, | ||
_tags = tags, | ||
_data = data | ||
}; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's on the line below (it got some ` on another PR)