Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 26 additions & 0 deletions TUnit.Core/ExternalSpanSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace TUnit.Core;

/// <summary>
/// Process-wide hook letting out-of-process span feeders (e.g. TUnit.OpenTelemetry's
/// OTLP receiver) push into TUnit.Engine's collector without OpenTelemetry referencing
/// Engine. Engine claims the slot at session start; first-wins semantics.
/// </summary>
internal static class ExternalSpanSink
{
private static Action<SpanData>? _sink;

// Volatile.Read pairs with the full fence in Interlocked.CompareExchange on the
// Register/Unregister side; without it, weak memory models (ARM) could observe
// a stale null after another thread has published a sink.
public static Action<SpanData>? Current => Volatile.Read(ref _sink);

public static void Register(Action<SpanData> sink)
{
Interlocked.CompareExchange(ref _sink, sink, null);
}

public static void Unregister(Action<SpanData> sink)
{
Interlocked.CompareExchange(ref _sink, null, sink);
}
}
78 changes: 78 additions & 0 deletions TUnit.Core/SpanData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Text.Json.Serialization;

namespace TUnit.Core;

internal sealed class SpanData
{
[JsonPropertyName("traceId")]
public required string TraceId { get; init; }

[JsonPropertyName("spanId")]
public required string SpanId { get; init; }

[JsonPropertyName("parentSpanId")]
public string? ParentSpanId { get; init; }

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("spanType")]
public string? SpanType { get; init; }

[JsonPropertyName("source")]
public required string Source { get; init; }

[JsonPropertyName("kind")]
public required string Kind { get; init; }

[JsonPropertyName("startTimeMs")]
public double StartTimeMs { get; init; }

[JsonPropertyName("durationMs")]
public double DurationMs { get; init; }

[JsonPropertyName("status")]
public required string Status { get; init; }

[JsonPropertyName("statusMessage")]
public string? StatusMessage { get; init; }

[JsonPropertyName("tags")]
public ReportKeyValue[]? Tags { get; init; }

[JsonPropertyName("events")]
public SpanEvent[]? Events { get; init; }

[JsonPropertyName("links")]
public SpanLink[]? Links { get; init; }
}

internal sealed class SpanEvent
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("timestampMs")]
public double TimestampMs { get; init; }

[JsonPropertyName("tags")]
public ReportKeyValue[]? Tags { get; init; }
}

internal sealed class SpanLink
{
[JsonPropertyName("traceId")]
public required string TraceId { get; init; }

[JsonPropertyName("spanId")]
public required string SpanId { get; init; }
}

internal sealed class ReportKeyValue
{
[JsonPropertyName("key")]
public required string Key { get; init; }

[JsonPropertyName("value")]
public required string Value { get; init; }
}
8 changes: 8 additions & 0 deletions TUnit.Engine/Reporters/Html/ActivityCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,16 @@ private static void WarnCapHitOnce()
// so that subsequent activities on the same trace avoid cross-class dictionary checks.
private readonly ConcurrentDictionary<string, byte> _knownTraceIds = new(StringComparer.OrdinalIgnoreCase);
private ActivityListener? _listener;
private Action<SpanData>? _externalSinkDelegate;

public void Start()
{
// First-started-wins: HtmlReporter creates one collector per session before any
// test runs, so this slot is claimed for the rest of the session. Later ad-hoc
// collectors (e.g. created from a test) don't race-steal the global pointer.
Interlocked.CompareExchange(ref _current, this, null);
_externalSinkDelegate = IngestExternalSpan;
ExternalSpanSink.Register(_externalSinkDelegate);
// Listen to ALL sources so we can capture child spans from HttpClient, ASP.NET Core,
// EF Core, etc. The Sample callback uses smart filtering to avoid overhead: only spans
// belonging to known test traces are fully recorded; everything else gets PropagationData
Expand Down Expand Up @@ -155,6 +158,11 @@ private ActivitySamplingResult SampleActivityUsingParentId(ref ActivityCreationO
public void Stop()
{
Interlocked.CompareExchange(ref _current, null, this);
if (_externalSinkDelegate is { } sink)
{
ExternalSpanSink.Unregister(sink);
_externalSinkDelegate = null;
}
_listener?.Dispose();
_listener = null;
}
Expand Down
75 changes: 1 addition & 74 deletions TUnit.Engine/Reporters/Html/HtmlReportDataModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using TUnit.Core;

namespace TUnit.Engine.Reporters.Html;

Expand Down Expand Up @@ -173,77 +174,3 @@ internal sealed class ReportExceptionData
public ReportExceptionData? InnerException { get; init; }
}

internal sealed class ReportKeyValue
{
[JsonPropertyName("key")]
public required string Key { get; init; }

[JsonPropertyName("value")]
public required string Value { get; init; }
}

internal sealed class SpanData
{
[JsonPropertyName("traceId")]
public required string TraceId { get; init; }

[JsonPropertyName("spanId")]
public required string SpanId { get; init; }

[JsonPropertyName("parentSpanId")]
public string? ParentSpanId { get; init; }

[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("spanType")]
public string? SpanType { get; init; }

[JsonPropertyName("source")]
public required string Source { get; init; }

[JsonPropertyName("kind")]
public required string Kind { get; init; }

[JsonPropertyName("startTimeMs")]
public double StartTimeMs { get; init; }

[JsonPropertyName("durationMs")]
public double DurationMs { get; init; }

[JsonPropertyName("status")]
public required string Status { get; init; }

[JsonPropertyName("statusMessage")]
public string? StatusMessage { get; init; }

[JsonPropertyName("tags")]
public ReportKeyValue[]? Tags { get; init; }

[JsonPropertyName("events")]
public SpanEvent[]? Events { get; init; }

[JsonPropertyName("links")]
public SpanLink[]? Links { get; init; }
}

internal sealed class SpanLink
{
[JsonPropertyName("traceId")]
public required string TraceId { get; init; }

[JsonPropertyName("spanId")]
public required string SpanId { get; init; }
}

internal sealed class SpanEvent
{
[JsonPropertyName("name")]
public required string Name { get; init; }

[JsonPropertyName("timestampMs")]
public double TimestampMs { get; init; }

[JsonPropertyName("tags")]
public ReportKeyValue[]? Tags { get; init; }
}
2 changes: 2 additions & 0 deletions TUnit.Engine/Reporters/Html/HtmlReportJsonContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using TUnit.Core;

namespace TUnit.Engine.Reporters.Html;

Expand All @@ -10,6 +11,7 @@ namespace TUnit.Engine.Reporters.Html;
[JsonSerializable(typeof(ReportKeyValue))]
[JsonSerializable(typeof(SpanData))]
[JsonSerializable(typeof(SpanEvent))]
[JsonSerializable(typeof(SpanLink))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Expand Down
1 change: 0 additions & 1 deletion TUnit.Engine/TUnit.Engine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<ItemGroup>
<InternalsVisibleTo Include="TUnit.UnitTests" />
<InternalsVisibleTo Include="TUnit.Engine.Tests" />
<InternalsVisibleTo Include="TUnit.OpenTelemetry" />
<InternalsVisibleTo Include="TUnit.OpenTelemetry.Tests" />
</ItemGroup>

Expand Down
7 changes: 3 additions & 4 deletions TUnit.OpenTelemetry/Receiver/OtlpReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Net;
using System.Net.Sockets;
using TUnit.Core;
using TUnit.Engine.Reporters.Html;

namespace TUnit.OpenTelemetry.Receiver;

Expand Down Expand Up @@ -212,8 +211,8 @@ private async Task ProcessRequestAsync(HttpListenerContext context)

private static void ProcessTraces(byte[] body)
{
var collector = ActivityCollector.Current;
if (collector is null)
var sink = ExternalSpanSink.Current;
if (sink is null)
{
return;
}
Expand All @@ -231,7 +230,7 @@ private static void ProcessTraces(byte[] body)

foreach (var span in spans)
{
collector.IngestExternalSpan(ToSpanData(span));
sink(ToSpanData(span));
}
}

Expand Down
2 changes: 1 addition & 1 deletion TUnit.OpenTelemetry/TUnit.OpenTelemetry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TUnit\TUnit.csproj" />
<ProjectReference Include="..\TUnit.Core\TUnit.Core.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading