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

Support performance #633

Merged
merged 55 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
f31adac
wip
Tyrrrz Nov 30, 2020
79d2397
wip
Tyrrrz Nov 30, 2020
d7c9974
wip
Tyrrrz Dec 1, 2020
51783e8
wip
Tyrrrz Dec 1, 2020
c99635d
wip
Tyrrrz Dec 1, 2020
7ad5ab5
wip
Tyrrrz Dec 2, 2020
96c443e
wip
Tyrrrz Dec 2, 2020
98e567d
wip
Tyrrrz Dec 2, 2020
e0eb730
wip
Tyrrrz Dec 2, 2020
748670c
wip
Tyrrrz Dec 2, 2020
dfdec31
wip
Tyrrrz Dec 2, 2020
fa64053
wip
Tyrrrz Dec 2, 2020
8338381
wip
Tyrrrz Dec 3, 2020
8fc5c2b
wip
Tyrrrz Dec 4, 2020
6a05871
wip
Tyrrrz Dec 4, 2020
6c0b928
wip
Tyrrrz Dec 4, 2020
5a47bd9
wip
Tyrrrz Dec 4, 2020
e67597e
wip
Tyrrrz Dec 7, 2020
52ec0a5
wip
Tyrrrz Dec 7, 2020
15d7001
wip
Tyrrrz Dec 7, 2020
3a705f0
wip
Tyrrrz Dec 8, 2020
c4f9371
wip
Tyrrrz Dec 8, 2020
00ef2b5
wip
Tyrrrz Dec 8, 2020
b0487cf
wip
Tyrrrz Dec 8, 2020
0a713f3
wip
Tyrrrz Dec 8, 2020
c314a66
wip
Tyrrrz Dec 8, 2020
9e2b7e1
Merge remote-tracking branch 'origin/main' into performance
Tyrrrz Dec 8, 2020
d0c5358
wip
Tyrrrz Dec 8, 2020
a4e01d8
wip
Tyrrrz Dec 8, 2020
d960d7f
wip
Tyrrrz Dec 8, 2020
a0fb93e
wip
Tyrrrz Dec 9, 2020
6c1b413
wip
Tyrrrz Dec 9, 2020
258ed5c
wip
Tyrrrz Dec 9, 2020
f2e5a8b
wip
Tyrrrz Dec 10, 2020
41bcb99
wip
Tyrrrz Dec 10, 2020
da7ee94
wip
Tyrrrz Dec 10, 2020
1371006
wip
Tyrrrz Dec 10, 2020
bafe82a
wip
Tyrrrz Dec 10, 2020
7abdb4f
wip
Tyrrrz Dec 11, 2020
a1e48f8
wip
Tyrrrz Dec 11, 2020
fd13150
wip
Tyrrrz Dec 11, 2020
134d0ec
wip
Tyrrrz Dec 14, 2020
6b92788
wip
Tyrrrz Dec 14, 2020
b6f3ee2
wip
Tyrrrz Dec 14, 2020
0e91066
wip
Tyrrrz Dec 14, 2020
ea06cfb
wip
Tyrrrz Dec 14, 2020
2cb49ef
wip
Tyrrrz Dec 14, 2020
e908cee
wip
Tyrrrz Dec 14, 2020
e8aa3cc
wip
Tyrrrz Dec 14, 2020
71c87ca
wip
Tyrrrz Dec 14, 2020
0c512b1
wip
Tyrrrz Dec 14, 2020
fbd6f17
wip
Tyrrrz Dec 14, 2020
13b88db
wip
Tyrrrz Dec 14, 2020
eb37814
wip
Tyrrrz Dec 14, 2020
61085a4
wip
Tyrrrz Dec 15, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Add net5.0 TFM to libraries #606
* Add more logging to CachingTransport #619
* Bump Microsoft.Bcl.AsyncInterfaces to 5.0.0 #618
Copy link
Member

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)

Suggested change
* Bump Microsoft.Bcl.AsyncInterfaces to 5.0.0 #618

* Add support for performance

## 3.0.0-alpha.5

Expand Down
9 changes: 9 additions & 0 deletions src/Sentry.AspNetCore/SentryMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The 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?

var transactionName = GetName(); // we get the name somewhere because it's added today to the event.transaction
var transaction = hub.StartTransaction(transactionName, new SamplingContext())

// Ideally we'd add to some data bag of the context, so we can get from it in different parts of the framework like some MVC Filters or something, and are able to create more spans from it, without having to rely on `AsyncLocal`:
context.SomeDataBag["SentryTransaction"] = transaction;

// ALternatively we rely on AsyncLocal, or both:
Hub.ConfigureScope(s => s.SetTransaction(transaction));

try {
var middlewareSpan = transaction.startSpan("middleware") (or startChild or something);
await middleware();
middlewareSpan.end();
transaction.status(context.Response.Status.ToSentryStatus());
} catch {
transaction.status(exception);
}
finally {
transaction.finish()
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var transaction = hub.StartTransaction(transactionName, new SamplingContext()) should rather take CustomSamplingContext instead of SamplingContext.

Hub.ConfigureScope(s => s.SetTransaction(transaction)); in Java once you start transaction it's set on the scoep automatically.

Other than that it does make sense.

Choose a reason for hiding this comment

The 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

  1. The transaction name is stored in the Scope (as there was already a slot for it there, and Tracing support hijacks that event field)
  2. The "some data bag" idea is represented by Go's Context type. Spans are stored there. There's no transaction type per-se. A transaction is a wire representation of a span tree (in fact, just a special type of Event).
  3. ⚠️ Bruno your code creates both a transaction AND a child span. But if you represent the transaction as a thing on its own, it is already a span. So in your example there's one too many nesting levels. Either the middleware starts a transaction or a child span on an existing transaction. I don't like code needing to check whether it needs to call StartTransaction or StartSpan, that's why, among other reasons, there's only StartSpan in Go.
  4. Apart from setting the span status, the Python SDK also sets a tag with the HTTP status. We should do that too.
    integrations/aiohttp.py#L112
    tracing.py#L319-L350
  5. When starting a transaction/span, we pass data that modify the transaction/span (the so called "TransactionContext" and "SpanContext" in JS). The "SamplingContext" is something else, it originates in the SDK and is passed to a TracesSampler.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One point to add:

  1. The op for HTTP server integrations should be "http.server", so we all follow a consistent naming pattern -- unless there's good reason to deviate from that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The op for HTTP server integrations should be "http.server", so we all follow a consistent naming pattern -- unless there's good reason to deviate from that.

Is this documented somewhere?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly not :/
It started off prior industry specs and we use it for Python as in integrations/aiohttp.py#L92

});

try
{
await _next(context).ConfigureAwait(false);
Expand All @@ -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)
{
Expand Down
11 changes: 5 additions & 6 deletions src/Sentry.AspNetCore/SentryStartupFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ namespace Sentry.AspNetCore
/// <inheritdoc />
internal class SentryStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
=> e =>
{
_ = e.UseSentry();
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => e =>
{
e.UseSentry();

next(e);
};
next(e);
};
}
}
23 changes: 23 additions & 0 deletions src/Sentry.AspNetCore/SpanStatusMapper.cs
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
};
}
}
7 changes: 7 additions & 0 deletions src/Sentry/ISentryTraceSampler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Sentry
{
public interface ISentryTraceSampler
{
double GetSampleRate();
}
}
9 changes: 9 additions & 0 deletions src/Sentry/Internal/Extensions/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ public static void WriteDictionaryValue(
}
}

public static void WriteDictionary(
this Utf8JsonWriter writer,
string propertyName,
IEnumerable<KeyValuePair<string, object?>>? dic)
{
writer.WritePropertyName(propertyName);
writer.WriteDictionaryValue(dic);
}

public static void WriteDictionary(
this Utf8JsonWriter writer,
string propertyName,
Expand Down
19 changes: 19 additions & 0 deletions src/Sentry/Protocol/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback)
return new Envelope(header, items);
}

/// <summary>
/// Creates an envelope that contains a single transaction.
/// </summary>
public static Envelope FromTransaction(Transaction transaction)
{
var header = new Dictionary<string, object>
{
// TODO: Is this right?
[EventIdKey] = transaction.SpanId.ToString()
};

var items = new[]
{
EnvelopeItem.FromTransaction(transaction)
};

return new Envelope(header, items);
}

private static async Task<IReadOnlyDictionary<string, object?>> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
Expand Down
26 changes: 26 additions & 0 deletions src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal sealed class EnvelopeItem : ISerializable, IDisposable
private const string TypeKey = "type";
private const string TypeValueEvent = "event";
private const string TypeValueUserReport = "user_report";
private const string TypeValueTransaction = "transaction";
private const string LengthKey = "length";
private const string FileNameKey = "file_name";

Expand Down Expand Up @@ -176,6 +177,19 @@ public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback)
return new EnvelopeItem(header, new JsonSerializable(sentryUserFeedback));
}

/// <summary>
/// Creates an envelope item from transaction.
/// </summary>
public static EnvelopeItem FromTransaction(Transaction transaction)
{
var header = new Dictionary<string, object>(StringComparer.Ordinal)
{
[TypeKey] = TypeValueTransaction
};

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

private static async Task<IReadOnlyDictionary<string, object?>> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -237,6 +251,18 @@ private static async Task<ISerializable> DeserializePayloadAsync(
);
}

// Transaction
if (string.Equals(payloadType, TypeValueTransaction, StringComparison.OrdinalIgnoreCase))
{
var bufferLength = (int)(payloadLength ?? stream.Length);
var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false);
using var jsonDocument = JsonDocument.Parse(buffer);

return new JsonSerializable(
Transaction.FromJson(jsonDocument.RootElement.Clone())
);
}

// Arbitrary payload
var payloadStream = new PartialStream(stream, stream.Position, payloadLength);

Expand Down
11 changes: 0 additions & 11 deletions src/Sentry/Protocol/IScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,6 @@ public interface IScope
/// </summary>
SentryLevel? Level { get; set; }

/// <summary>
/// The name of the transaction in which there was an event.
/// </summary>
/// <remarks>
/// A transaction should only be defined when it can be well defined.
/// On a Web framework, for example, a transaction is the route template
/// rather than the actual request path. That is so GET /user/10 and /user/20
/// (which have route template /user/{id}) are identified as the same transaction.
/// </remarks>
string? Transaction { get; set; }

/// <summary>
/// Gets or sets the HTTP.
/// </summary>
Expand Down
34 changes: 34 additions & 0 deletions src/Sentry/Protocol/ISpan.cs
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();
}
}
10 changes: 9 additions & 1 deletion src/Sentry/Protocol/SentryEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,15 @@ public IEnumerable<SentryThread>? SentryThreads
/// <inheritdoc />
public SentryLevel? Level { get; set; }

/// <inheritdoc />
/// <summary>
/// The name of the transaction in which there was an event.
/// </summary>
/// <remarks>
/// A transaction should only be defined when it can be well defined.
/// On a Web framework, for example, a transaction is the route template
/// rather than the actual request path. That is so GET /user/10 and /user/20
/// (which have route template /user/{id}) are identified as the same transaction.
/// </remarks>
public string? Transaction { get; set; }

private Request? _request;
Expand Down
5 changes: 5 additions & 0 deletions src/Sentry/Protocol/SentryId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ namespace Sentry.Protocol
/// <inheritdoc />
public override int GetHashCode() => _eventId.GetHashCode();

/// <summary>
/// Generates a new Sentry ID.
/// </summary>
public static SentryId Create() => new SentryId(Guid.NewGuid());

/// <inheritdoc />
public void WriteTo(Utf8JsonWriter writer) => writer.WriteStringValue(ToString());

Expand Down
116 changes: 116 additions & 0 deletions src/Sentry/Protocol/Span.cs
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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol really has all of these members being mutable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.
Ideally things would be immutable unless they need not to be.

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>();
Copy link
Member

Choose a reason for hiding this comment

The 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 get get the same instance. Maybe a double check lock will do

Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

@Tyrrrz Tyrrrz Dec 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other one will cost when accessing it only and only the first time due to the double check lock.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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...
https://develop.sentry.dev/sdk/event-payloads/span/

Copy link
Member

Choose a reason for hiding this comment

The 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>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Lazy with PublicationOnly

Copy link
Contributor Author

@Tyrrrz Tyrrrz Dec 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we can't seed it with data during deserialization

Copy link
Member

Choose a reason for hiding this comment

The 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 volatile. Perhaps a double check lock or Interlocked.Exchange could solve the problem here but I think Lazy<T> with PublicationOnly` is the simplest.

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();
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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))
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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
};
}
}
}
Loading