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

[.NET][Parametric] add baggage endpoints, enable baggage tests #3544

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion manifests/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ tests/:
TestDynamicConfigV1_ServiceTargets: missing_feature
TestDynamicConfigV2: v2.44.0
test_headers_baggage.py:
Test_Headers_Baggage: missing_feature
Test_Headers_Baggage: v3.6.0
test_otel_api_interoperability.py: missing_feature
test_otel_env_vars.py:
Test_Otel_Env_Vars: v2.53.0
Expand Down
2 changes: 1 addition & 1 deletion utils/_context/_scenarios/parametric.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ def dotnet_library_factory():
RUN /binaries/install_ddtrace.sh

# dotnet restore
COPY {dotnet_reldir}/ApmTestApi.csproj {dotnet_reldir}/nuget.config ./
COPY {dotnet_reldir}/ApmTestApi.csproj {dotnet_reldir}/nuget.config {dotnet_reldir}/*.nupkg ./
RUN dotnet restore "./ApmTestApi.csproj"

# dotnet publish
Expand Down
150 changes: 112 additions & 38 deletions utils/build/docker/dotnet/parametric/Endpoints/ApmTestApi.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Datadog.Trace;
using System.Reflection;
using System.Threading;
using ApmTestApi.ExtensionMethods;
using Newtonsoft.Json;

namespace ApmTestApi.Endpoints;
Expand All @@ -27,57 +27,54 @@ public static void MapApmTraceEndpoints(WebApplication app, ILogger<ApmTestApi>
app.MapPost("/trace/span/set_metric", SpanSetMetric);
app.MapPost("/trace/span/finish", FinishSpan);
app.MapPost("/trace/span/flush", FlushSpans);

// baggage
app.MapPost("/trace/span/set_baggage", SetBaggage);
app.MapGet("/trace/span/get_baggage", GetBaggage);
app.MapGet("/trace/span/get_all_baggage", GetAllBaggage);
app.MapPost("/trace/span/remove_baggage", RemoveBaggage);
app.MapPost("/trace/span/remove_all_baggage", RemoveAllBaggage);
}

// Core types
private static readonly Type SpanType = Type.GetType("Datadog.Trace.Span, Datadog.Trace", throwOnError: true)!;
private static readonly Type SpanContextType = Type.GetType("Datadog.Trace.SpanContext, Datadog.Trace", throwOnError: true)!;
private static readonly Type TracerType = Type.GetType("Datadog.Trace.Tracer, Datadog.Trace", throwOnError: true)!;
private static readonly Type TracerManagerType = Type.GetType("Datadog.Trace.TracerManager, Datadog.Trace", throwOnError: true)!;
private static readonly Type GlobalSettingsType = Type.GetType("Datadog.Trace.Configuration.GlobalSettings, Datadog.Trace", throwOnError: true)!;
private static readonly Type ImmutableTracerSettingsType = Type.GetType("Datadog.Trace.Configuration.ImmutableTracerSettings, Datadog.Trace", throwOnError: true)!;

// Propagator types
internal static readonly Type W3CTraceContextPropagatorType = Type.GetType("Datadog.Trace.Propagators.W3CTraceContextPropagator, Datadog.Trace", throwOnError: true)!;

// Agent-related types
private static readonly Type AgentWriterType = Type.GetType("Datadog.Trace.Agent.AgentWriter, Datadog.Trace", throwOnError: true)!;
internal static readonly Type StatsAggregatorType = Type.GetType("Datadog.Trace.Agent.StatsAggregator, Datadog.Trace", throwOnError: true)!;
private static readonly Type StatsAggregatorType = Type.GetType("Datadog.Trace.Agent.StatsAggregator, Datadog.Trace", throwOnError: true)!;

// Accessors for internal properties/fields accessors
internal static readonly PropertyInfo GetGlobalSettingsInstance = GlobalSettingsType.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo GetTracerManager = TracerType.GetProperty("TracerManager", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly MethodInfo GetAgentWriter = TracerManagerType.GetProperty("AgentWriter", BindingFlags.Instance | BindingFlags.Public)!.GetGetMethod()!;
internal static readonly FieldInfo GetStatsAggregator = AgentWriterType.GetField("_statsAggregator", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly PropertyInfo SpanContext = SpanType.GetProperty("Context", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly PropertyInfo Origin = SpanContextType.GetProperty("Origin", BindingFlags.Instance | BindingFlags.NonPublic)!;

internal static readonly PropertyInfo SamplingPriority = SpanContextType.GetProperty("SamplingPriority", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo RawTraceId = SpanContextType.GetProperty("RawTraceId", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo RawSpanId = SpanContextType.GetProperty("RawSpanId", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo AdditionalW3CTraceState = SpanContextType.GetProperty("AdditionalW3CTraceState", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo PropagationStyleInject = ImmutableTracerSettingsType.GetProperty("PropagationStyleInject", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo RuntimeMetricsEnabled = ImmutableTracerSettingsType.GetProperty("RuntimeMetricsEnabled", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo IsActivityListenerEnabled = ImmutableTracerSettingsType.GetProperty("IsActivityListenerEnabled", BindingFlags.Instance | BindingFlags.NonPublic)!;
internal static readonly PropertyInfo GetTracerInstance = TracerType.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)!;
internal static readonly PropertyInfo GetTracerSettings = TracerType.GetProperty("Settings", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!;
internal static readonly PropertyInfo GetDebugEnabled = GlobalSettingsType.GetProperty("DebugEnabled", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!;

// Propagator methods
internal static readonly MethodInfo W3CTraceContextCreateTraceStateHeader = W3CTraceContextPropagatorType.GetMethod("CreateTraceStateHeader", BindingFlags.Static | BindingFlags.NonPublic)!;
private static readonly PropertyInfo GetGlobalSettingsInstance = GlobalSettingsType.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic)!;
private static readonly PropertyInfo GetTracerManager = TracerType.GetProperty("TracerManager", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly MethodInfo GetAgentWriter = TracerManagerType.GetProperty("AgentWriter", BindingFlags.Instance | BindingFlags.Public)!.GetGetMethod()!;
private static readonly FieldInfo GetStatsAggregator = AgentWriterType.GetField("_statsAggregator", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly PropertyInfo PropagationStyleInject = ImmutableTracerSettingsType.GetProperty("PropagationStyleInject", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly PropertyInfo RuntimeMetricsEnabled = ImmutableTracerSettingsType.GetProperty("RuntimeMetricsEnabled", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly PropertyInfo IsActivityListenerEnabled = ImmutableTracerSettingsType.GetProperty("IsActivityListenerEnabled", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static readonly PropertyInfo GetTracerInstance = TracerType.GetProperty("Instance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)!;
private static readonly PropertyInfo GetTracerSettings = TracerType.GetProperty("Settings", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!;
private static readonly PropertyInfo GetDebugEnabled = GlobalSettingsType.GetProperty("DebugEnabled", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!;

// StatsAggregator flush methods
private static readonly MethodInfo StatsAggregatorDisposeAsync = StatsAggregatorType.GetMethod("DisposeAsync", BindingFlags.Instance | BindingFlags.Public)!;
private static readonly MethodInfo StatsAggregatorFlush = StatsAggregatorType.GetMethod("Flush", BindingFlags.Instance | BindingFlags.NonPublic)!;

private static readonly Dictionary<ulong, ISpan> Spans = new();
private static readonly Dictionary<ulong, Datadog.Trace.ISpanContext> DDContexts = new();
private static readonly Dictionary<ulong, ISpanContext> DDContexts = new();

internal static ILogger<ApmTestApi>? _logger;

internal static readonly SpanContextInjector _spanContextInjector = new();
internal static readonly SpanContextExtractor _spanContextExtractor = new();



// persist a single baggage collection across requests (for testing purposes)
private static readonly IDictionary<string, string?> _baggage = Baggage.Current;

internal static IEnumerable<string> GetHeaderValues(string[][] headersList, string key)
{
List<string> values = new List<string>();
Expand Down Expand Up @@ -186,7 +183,19 @@ private static async Task SpanSetMetric(HttpRequest request)

private static async Task SpanSetError(HttpRequest request)
{
var span = Spans[Convert.ToUInt64(await FindBodyKeyValueAsync(request, "span_id"))];
var requestJson = await JsonHelper.Create(request.Body);
var spanId = requestJson.GetUInt64("span_id");

if (spanId is null)
{
throw new InvalidOperationException("span_id not found in request json.");
}

if (!Spans.TryGetValue(spanId.Value, out var span))
{
throw new InvalidOperationException($"Span not found. span_id: {spanId}");
}

span.Error = true;

var type = await FindBodyKeyValueAsync(request, "type");
Expand Down Expand Up @@ -219,7 +228,7 @@ private static async Task<string> ExtractHeaders(HttpRequest request)
getter: GetHeaderValues
);

String extractedSpanId = null;
string? extractedSpanId = null;
if (extractedContext is not null)
{
DDContexts[extractedContext.SpanId] = extractedContext;
Expand All @@ -244,10 +253,7 @@ private static async Task<string> InjectHeaders(HttpRequest request)
static void Setter(List<string[]> headers, string key, string value) =>
headers.Add(new string[] { key, value });

Console.WriteLine(JsonConvert.SerializeObject(new
{
HttpHeaders = httpHeaders
}));
_logger?.LogInformation("StartSpan: {HeaderRequestBody}", headerRequestBody);

// Invoke SpanContextPropagator.Inject with the HttpRequestHeaders
_spanContextInjector.Inject(httpHeaders, Setter, span.Context);
Expand Down Expand Up @@ -360,10 +366,78 @@ internal static async Task FlushTraceStats()

internal static async Task<string> FindBodyKeyValueAsync(HttpRequest httpRequest, string keyToFind)
{
var headerBodyDictionary = await new StreamReader(httpRequest.Body).ReadToEndAsync();
var parsedDictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(headerBodyDictionary);
var keyFound = parsedDictionary!.TryGetValue(keyToFind, out var foundValue);
var requestJson = await JsonHelper.Create(httpRequest.Body);
return requestJson.GetString(keyToFind);
}

private static async Task SetBaggage(HttpRequest request)
{
var requestHelper = await JsonHelper.Create(request.Body);
var key = requestHelper.GetString("key");
var value = requestHelper.GetString("value");

if (key is null || value is null)
{
throw new InvalidOperationException("Key and value are required to set baggage item");
}

// restore baggage from previous request
Baggage.Current = _baggage;

// set new baggage item
Baggage.Current[key] = value;
}

private static async Task<string> GetBaggage(HttpRequest request)
{
var requestHelper = await JsonHelper.Create(request.Body);
var key = requestHelper.GetString("key");

if (key is null)
{
throw new InvalidOperationException("Key is required to get baggage item");
}

// restore baggage from previous request
Baggage.Current = _baggage;

// get baggage item by key
var value = Baggage.Current.GetValueOrDefault(key);
return JsonConvert.SerializeObject(new { baggage = value });
}

private static string GetAllBaggage(HttpRequest request)
{
// restore baggage from previous request
Baggage.Current = _baggage;

// get all baggage items
return JsonConvert.SerializeObject(new { baggage = Baggage.Current });
}

private static async Task RemoveBaggage(HttpRequest request)
{
var requestHelper = await JsonHelper.Create(request.Body);
var key = requestHelper.GetString("key");

if (key is null)
{
throw new InvalidOperationException("Key is required to remove baggage item");
}

// restore baggage from previous request
Baggage.Current = _baggage;

// remove baggage item by key
Baggage.Current.Remove(key);
}

private static void RemoveAllBaggage(HttpRequest request)
{
// restore baggage from previous request
Baggage.Current = _baggage;

return keyFound ? foundValue! : String.Empty;
// remove all baggage items
Baggage.Current.Clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ApmTestApi.ExtensionMethods;

public static class DictionaryExtensions
{
public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
{
return dictionary.TryGetValue(key, out var value) ? value : default;
}
}
42 changes: 42 additions & 0 deletions utils/build/docker/dotnet/parametric/JsonHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Text.Json;

namespace ApmTestApi;

public class JsonHelper
{
private readonly JsonElement _json;

private JsonHelper(JsonElement json)
{
_json = json;
}

public static async Task<JsonHelper> Create(Stream stream)
{
var requestData = await JsonDocument.ParseAsync(stream);

if (requestData is null)
{
throw new InvalidDataException("Invalid request body. Expected JSON.");
}

return new JsonHelper(requestData.RootElement);
}

public string? GetString(string key)
{
return _json.GetProperty(key).GetString();
}

public ulong? GetUInt64(string key)
{
var property = _json.GetProperty(key);

return property.ValueKind switch
{
JsonValueKind.Number => property.GetUInt64(),
JsonValueKind.String when ulong.TryParse(property.GetString(), out var value) => value,
_ => null
};
}
}
2 changes: 1 addition & 1 deletion utils/build/docker/dotnet/parametric/nuget.config
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<packageSources>
<!--To inherit the global NuGet package sources remove the <clear/> line below -->
<clear />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="local" value="./" />
<add key="nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
Loading