Skip to content

Commit a491075

Browse files
Added protocol classes
1 parent f95f266 commit a491075

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

src/Sentry/Protocol/Envelopes/Envelope.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,4 +504,20 @@ internal Envelope WithItem(EnvelopeItem item)
504504
items.Add(item);
505505
return new Envelope(_eventId, Header, items);
506506
}
507+
508+
/// <summary>
509+
/// Creates an envelope that contains one or more Span v2 items.
510+
/// </summary>
511+
internal static Envelope FromSpans(IReadOnlyCollection<SpanV2> spans)
512+
{
513+
var header = DefaultHeader;
514+
515+
var spanItems = new SpanV2Items(spans);
516+
517+
var items = spanItems.Length > 0
518+
? new List<EnvelopeItem>(1) { EnvelopeItem.FromSpans(spanItems) }
519+
: new List<EnvelopeItem>(0);
520+
521+
return new Envelope(header, items);
522+
}
507523
}

src/Sentry/Protocol/Envelopes/EnvelopeItem.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,21 @@ public static EnvelopeItem FromSession(SessionUpdate sessionUpdate)
299299
return new EnvelopeItem(header, new JsonSerializable(sessionUpdate));
300300
}
301301

302+
/// <summary>
303+
/// Creates an <see cref="EnvelopeItem"/> from one or more <paramref name="spans"/>.
304+
/// </summary>
305+
internal static EnvelopeItem FromSpans(SpanV2Items spans)
306+
{
307+
var header = new Dictionary<string, object?>(3, StringComparer.Ordinal)
308+
{
309+
[TypeKey] = TypeValueSpan,
310+
["item_count"] = spans.Length,
311+
["content_type"] = "application/vnd.sentry.items.span+json",
312+
};
313+
314+
return new EnvelopeItem(header, new JsonSerializable(spans));
315+
}
316+
302317
/// <summary>
303318
/// Creates an <see cref="EnvelopeItem"/> from <paramref name="checkIn"/>.
304319
/// </summary>

src/Sentry/Protocol/SpanV2.cs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using Sentry.Extensibility;
2+
using Sentry.Internal.Extensions;
3+
using Sentry.Protocol.Metrics;
4+
5+
namespace Sentry.Protocol;
6+
7+
/// <summary>
8+
/// Represents a single Span (Span v2 protocol) to be sent in a dedicated span envelope item.
9+
/// </summary>
10+
/// <remarks>
11+
/// Developer docs: https://develop.sentry.dev/sdk/telemetry/spans/span-protocol/
12+
/// </remarks>
13+
internal sealed class SpanV2 : ISentryJsonSerializable
14+
{
15+
public const int MaxSpansPerEnvelope = 100;
16+
17+
public SpanV2(
18+
SentryId traceId,
19+
SpanId spanId,
20+
string operation,
21+
DateTimeOffset startTimestamp)
22+
{
23+
TraceId = traceId;
24+
SpanId = spanId;
25+
Operation = operation;
26+
StartTimestamp = startTimestamp;
27+
}
28+
29+
public SentryId TraceId { get; }
30+
public SpanId SpanId { get; }
31+
public SpanId? ParentSpanId { get; set; }
32+
33+
/// <summary>
34+
/// The span operation.
35+
/// </summary>
36+
public string Operation { get; set; }
37+
38+
public string? Description { get; set; }
39+
public SpanStatus? Status { get; set; }
40+
41+
public DateTimeOffset StartTimestamp { get; }
42+
public DateTimeOffset? EndTimestamp { get; set; }
43+
44+
public string? Origin { get; set; }
45+
46+
public string? SegmentId { get; set; }
47+
48+
public bool? IsSampled { get; set; }
49+
50+
private Dictionary<string, string>? _tags;
51+
public IReadOnlyDictionary<string, string> Tags => _tags ??= new Dictionary<string, string>();
52+
53+
private Dictionary<string, object?>? _data;
54+
public IReadOnlyDictionary<string, object?> Data => _data ??= new Dictionary<string, object?>();
55+
56+
private Dictionary<string, Measurement>? _measurements;
57+
public IReadOnlyDictionary<string, Measurement> Measurements => _measurements ??= new Dictionary<string, Measurement>();
58+
59+
private MetricsSummary? _metricsSummary;
60+
61+
public static SpanV2 FromSpan(ISpan span) => new(span.TraceId, span.SpanId, span.Operation, span.StartTimestamp)
62+
{
63+
ParentSpanId = span.ParentSpanId,
64+
Description = span.Description,
65+
Status = span.Status,
66+
EndTimestamp = span.EndTimestamp,
67+
Origin = span.Origin,
68+
IsSampled = span.IsSampled,
69+
SegmentId = null, // reserved for future SDK behavior
70+
_tags = span.Tags.ToDict(),
71+
_data = span.Data.ToDict(),
72+
_measurements = span.Measurements.ToDict(),
73+
};
74+
75+
public void SetTag(string key, string value) => (_tags ??= new Dictionary<string, string>())[key] = value;
76+
public void SetData(string key, object? value) => (_data ??= new Dictionary<string, object?>())[key] = value;
77+
public void SetMeasurement(string name, Measurement measurement) => (_measurements ??= new Dictionary<string, Measurement>())[name] = measurement;
78+
internal void SetMetricsSummary(MetricsSummary summary) => _metricsSummary = summary;
79+
80+
public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
81+
{
82+
writer.WriteStartObject();
83+
84+
writer.WriteSerializable("trace_id", TraceId, logger);
85+
writer.WriteSerializable("span_id", SpanId, logger);
86+
writer.WriteSerializableIfNotNull("parent_span_id", ParentSpanId, logger);
87+
88+
writer.WriteStringIfNotWhiteSpace("op", Operation);
89+
writer.WriteStringIfNotWhiteSpace("description", Description);
90+
writer.WriteStringIfNotWhiteSpace("status", Status?.ToString().ToSnakeCase());
91+
92+
// Span v2 uses the same timestamp format as other payloads in this SDK.
93+
writer.WriteString("start_timestamp", StartTimestamp);
94+
writer.WriteStringIfNotNull("timestamp", EndTimestamp);
95+
96+
writer.WriteStringIfNotWhiteSpace("origin", Origin);
97+
writer.WriteStringIfNotWhiteSpace("segment_id", SegmentId);
98+
99+
if (IsSampled is { } sampled)
100+
{
101+
writer.WriteBoolean("sampled", sampled);
102+
}
103+
104+
writer.WriteStringDictionaryIfNotEmpty("tags", _tags!);
105+
writer.WriteDictionaryIfNotEmpty("data", _data!, logger);
106+
writer.WriteDictionaryIfNotEmpty("measurements", _measurements, logger);
107+
writer.WriteSerializableIfNotNull("_metrics_summary", _metricsSummary, logger);
108+
109+
writer.WriteEndObject();
110+
}
111+
}
112+
113+
/// <summary>
114+
/// Span v2 envelope item payload.
115+
/// </summary>
116+
/// <remarks>
117+
/// Developer docs: https://develop.sentry.dev/sdk/telemetry/spans/span-protocol/
118+
/// </remarks>
119+
internal sealed class SpanV2Items : ISentryJsonSerializable
120+
{
121+
private readonly IReadOnlyCollection<SpanV2> _spans;
122+
123+
public SpanV2Items(IReadOnlyCollection<SpanV2> spans)
124+
{
125+
_spans = (spans.Count > SpanV2.MaxSpansPerEnvelope)
126+
? [..spans.Take(SpanV2.MaxSpansPerEnvelope)]
127+
: spans;
128+
}
129+
130+
public int Length => _spans.Count;
131+
132+
public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger)
133+
{
134+
writer.WriteStartObject();
135+
writer.WriteStartArray("items");
136+
137+
foreach (var span in _spans)
138+
{
139+
span.WriteTo(writer, logger);
140+
}
141+
142+
writer.WriteEndArray();
143+
writer.WriteEndObject();
144+
}
145+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using Sentry.Protocol.Spans;
2+
3+
namespace Sentry.Tests.Protocol.Envelopes;
4+
5+
public class SpanV2EnvelopeTests
6+
{
7+
[Fact]
8+
public async Task EnvelopeItem_FromSpanV2_SerializesHeaderWithContentTypeAndItemCount()
9+
{
10+
var span = new SpanV2(SentryId.Parse("0123456789abcdef0123456789abcdef"), SpanId.Parse("0123456789abcdef"), "db", DateTimeOffset.Parse("2020-01-01T00:00:00Z"))
11+
{
12+
Description = "select 1",
13+
Status = SpanStatus.Ok,
14+
EndTimestamp = DateTimeOffset.Parse("2020-01-01T00:00:01Z"),
15+
};
16+
17+
using var envelopeItem = EnvelopeItem.FromSpanV2(span);
18+
19+
using var stream = new MemoryStream();
20+
await envelopeItem.SerializeAsync(stream, null);
21+
stream.Position = 0;
22+
using var reader = new StreamReader(stream);
23+
var output = await reader.ReadToEndAsync();
24+
25+
var firstLine = output.Split('\n', StringSplitOptions.RemoveEmptyEntries)[0];
26+
firstLine.Should().Contain("\"type\":\"span_v2\"");
27+
firstLine.Should().Contain("\"item_count\":1");
28+
firstLine.Should().Contain("\"content_type\":\"application/vnd.sentry.items.span\\u002Bjson\"");
29+
}
30+
31+
[Fact]
32+
public void Envelope_FromSpanV2_ThrowsWhenMoreThan100Spans()
33+
{
34+
var spans = Enumerable.Range(0, 101).Select(_ =>
35+
new SpanV2(SentryId.Create(), SpanId.Create(), "op", DateTimeOffset.UtcNow));
36+
37+
Action act = () => Envelope.FromSpanV2(spans);
38+
39+
act.Should().Throw<ArgumentOutOfRangeException>();
40+
}
41+
42+
[Fact]
43+
public void Envelope_FromSpanV2_CreatesUpTo100Items()
44+
{
45+
var spans = Enumerable.Range(0, 100).Select(_ =>
46+
new SpanV2(SentryId.Create(), SpanId.Create(), "op", DateTimeOffset.UtcNow)).ToList();
47+
48+
using var envelope = Envelope.FromSpanV2(spans);
49+
50+
envelope.Items.Should().HaveCount(100);
51+
envelope.Items.All(i => i.TryGetType() == "span_v2").Should().BeTrue();
52+
}
53+
}

0 commit comments

Comments
 (0)