From f7e913d22e8d02e7b7e0cba9bf479f2cb1378069 Mon Sep 17 00:00:00 2001 From: Tony Redondo Date: Tue, 21 Oct 2025 15:01:28 +0200 Subject: [PATCH] ducktype collecting --- .../build/Datadog.Trace.Trimming.xml | 1 + tracer/src/Datadog.Trace/Datadog.Trace.csproj | 1 + .../src/Datadog.Trace/DuckTyping/DuckType.cs | 10 + .../DuckTyping/DuckTypeReporter.cs | 328 ++++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 tracer/src/Datadog.Trace/DuckTyping/DuckTypeReporter.cs diff --git a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml index d689d08049a0..fe6a1e14ce6f 100644 --- a/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml +++ b/tracer/src/Datadog.Trace.Trimming/build/Datadog.Trace.Trimming.xml @@ -390,6 +390,7 @@ + diff --git a/tracer/src/Datadog.Trace/Datadog.Trace.csproj b/tracer/src/Datadog.Trace/Datadog.Trace.csproj index f7c0749e6db7..496d5bb13a22 100644 --- a/tracer/src/Datadog.Trace/Datadog.Trace.csproj +++ b/tracer/src/Datadog.Trace/Datadog.Trace.csproj @@ -49,6 +49,7 @@ + diff --git a/tracer/src/Datadog.Trace/DuckTyping/DuckType.cs b/tracer/src/Datadog.Trace/DuckTyping/DuckType.cs index e4877e0a2b8b..8229a1dc231b 100644 --- a/tracer/src/Datadog.Trace/DuckTyping/DuckType.cs +++ b/tracer/src/Datadog.Trace/DuckTyping/DuckType.cs @@ -225,6 +225,11 @@ private static CreateTypeResult CreateProxyType(Type proxyDefinitionType, Type t // Create Type Type proxyType = proxyTypeBuilder!.CreateTypeInfo()!.AsType(); + if (!dryRun) + { + DuckTypeReporter.ReportDuckType(proxyDefinitionType, targetType); + } + return new CreateTypeResult(proxyDefinitionType, proxyType, targetType, GetCreateProxyInstanceDelegate(moduleBuilder, proxyDefinitionType, proxyType, targetType), null); } } @@ -306,6 +311,11 @@ private static CreateTypeResult CreateReverseProxyType(Type typeToDeriveFrom, Ty // Create Type Type? proxyType = proxyTypeBuilder!.CreateTypeInfo()!.AsType(); + if (!dryRun) + { + DuckTypeReporter.ReportDuckType(typeToDeriveFrom, typeToDelegateTo); + } + return new CreateTypeResult(typeToDeriveFrom, proxyType, typeToDelegateTo, GetCreateProxyInstanceDelegate(moduleBuilder, typeToDeriveFrom, proxyType, typeToDelegateTo), null); } catch (DuckTypeException ex) diff --git a/tracer/src/Datadog.Trace/DuckTyping/DuckTypeReporter.cs b/tracer/src/Datadog.Trace/DuckTyping/DuckTypeReporter.cs new file mode 100644 index 000000000000..807b6e3778aa --- /dev/null +++ b/tracer/src/Datadog.Trace/DuckTyping/DuckTypeReporter.cs @@ -0,0 +1,328 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Datadog.Trace.Logging; +using Datadog.Trace.Vendors.Newtonsoft.Json; + +namespace Datadog.Trace.DuckTyping; + +internal static class DuckTypeReporter +{ + private const int MaxBufferBytes = 1 * 1024 * 1024; + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(DuckTypeReporter)); + private static readonly Uri Endpoint = ResolveEndpoint(); + private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(5); + private static readonly HttpClient HttpClient = new(); + private static readonly object SyncRoot = new(); + private static readonly List Buffer = new(); + private static readonly Timer FlushTimer = new(_ => Task.Run(() => FlushInternalAsync(CancellationToken.None)), null, FlushInterval, FlushInterval); + + private static readonly JsonSerializerSettings SerializerSettings = new() + { + NullValueHandling = NullValueHandling.Include + }; + + private static readonly Task CompletedTask = Task.FromResult(0); + + private static int _bufferBytes; + private static bool _flushInFlight; + + static DuckTypeReporter() + { + LifetimeManager.Instance.AddAsyncShutdownTask(_ => FlushAsync()); + } + + public static void ReportDuckType(Type proxyType, Type targetType) + { + if (proxyType == null) + { + throw new ArgumentNullException(nameof(proxyType)); + } + + if (targetType == null) + { + throw new ArgumentNullException(nameof(targetType)); + } + + var parentType = ResolveParentType(targetType); + + var proxyAssemblyQualifiedName = proxyType.AssemblyQualifiedName ?? proxyType.FullName ?? string.Empty; + var targetAssemblyName = targetType.Assembly.GetName().Name ?? string.Empty; + var targetTypeName = targetType.FullName ?? targetType.Name ?? string.Empty; + + var parentTargetAssemblyName = string.Empty; + var parentTargetTypeName = string.Empty; + if (parentType != null) + { + var parentAssemblyName = parentType.Assembly.GetName().Name; + if (!string.IsNullOrEmpty(parentAssemblyName)) + { + parentTargetAssemblyName = parentAssemblyName; + } + + var parentFullName = parentType.FullName; + if (!string.IsNullOrEmpty(parentFullName)) + { + parentTargetTypeName = parentFullName; + } + else if (!string.IsNullOrEmpty(parentType.Name)) + { + parentTargetTypeName = parentType.Name; + } + } + + var record = new RecordPayload + { + ProxyAssemblyQualifiedName = proxyAssemblyQualifiedName, + TargetAssemblyName = targetAssemblyName, + TargetTypeName = targetTypeName, + ParentTargetAssemblyName = parentTargetAssemblyName, + ParentTargetTypeName = parentTargetTypeName, + }; + + var recordBytes = EstimateSize(record); + + List? toFlush = null; + + lock (SyncRoot) + { + Buffer.Add(record); + _bufferBytes += recordBytes; + + if (_bufferBytes >= MaxBufferBytes) + { + toFlush = DrainBuffer(); + } + } + + if (toFlush != null) + { + _ = Task.Run(() => SendWithRetryAsync(toFlush, CancellationToken.None)) + .ContinueWith( + t => + { + if (t.IsFaulted) + { + var exception = t.Exception?.Flatten(); + var message = exception is { InnerException: not null } ? exception.InnerException.Message : "unknown error"; + Log.Error(exception, "DuckTypeReporter flush failed: {0}", message); + } + }, + TaskScheduler.Default); + } + } + + public static Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + List? toFlush; + + lock (SyncRoot) + { + toFlush = DrainBuffer(); + if (toFlush == null || toFlush.Count == 0) + { + return CompletedTask; + } + } + + return SendWithRetryAsync(toFlush, cancellationToken); + } + + private static Task FlushInternalAsync(CancellationToken cancellationToken) + { + List? toFlush; + + lock (SyncRoot) + { + if (_flushInFlight) + { + return CompletedTask; + } + + toFlush = DrainBuffer(); + if (toFlush == null || toFlush.Count == 0) + { + return CompletedTask; + } + + _flushInFlight = true; + } + + return SendWithRetryAsync(toFlush, cancellationToken) + .ContinueWith( + task => + { + lock (SyncRoot) + { + _flushInFlight = false; + } + + if (task.IsFaulted) + { + var exception = task.Exception?.Flatten(); + var message = exception is { InnerException: not null } + ? exception.InnerException.Message + : "unknown error"; + Log.Error(exception, "DuckTypeReporter flush failed: {0}", message); + } + }, + TaskScheduler.Default); + } + + private static async Task SendWithRetryAsync(List payload, CancellationToken cancellationToken) + { + if (payload.Count == 0) + { + return; + } + + const int maxAttempts = 5; + var delay = TimeSpan.FromSeconds(1); + var json = JsonConvert.SerializeObject(payload, SerializerSettings); + var payloadBytes = Encoding.UTF8.GetByteCount(json); + + for (var attempt = 1; attempt <= maxAttempts; attempt++) + { + using (var content = new StringContent(json, Encoding.UTF8, "application/json")) + { + try + { + using (var request = new HttpRequestMessage(HttpMethod.Post, Endpoint)) + { + request.Content = content; + + using (var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false)) + { + if (response.IsSuccessStatusCode) + { + return; + } + + await HandleErrorAsync(response).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + Log.Error("DuckTypeReporter attempt {0} failed: {1}", attempt, ex.Message); + } + } + + if (attempt < maxAttempts) + { + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); + } + } + + lock (SyncRoot) + { + Buffer.AddRange(payload); + _bufferBytes += payloadBytes; + } + + throw new InvalidOperationException("Failed to report duck type payload after multiple attempts."); + } + + private static Uri ResolveEndpoint() + { + var endpoint = Environment.GetEnvironmentVariable("DUCKTYPE_ENDPOINT"); + if (!string.IsNullOrWhiteSpace(endpoint)) + { + return new Uri(endpoint); + } + + var portValue = Environment.GetEnvironmentVariable("DUCKTYPE_PORT") ?? + Environment.GetEnvironmentVariable("PORT"); + + int port; + if (!string.IsNullOrWhiteSpace(portValue) && int.TryParse(portValue, out port) && port > 0) + { + return new Uri($"http://localhost:{port}/records"); + } + + return new Uri("https://tough-badgers-kneel.loca.lt/records"); + } + + private static async Task HandleErrorAsync(HttpResponseMessage response) + { + var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + Log.Error("DuckTypeReporter received {0}: {1}", (int)response.StatusCode, body); + } + + private static List? DrainBuffer() + { + if (Buffer.Count == 0) + { + return null; + } + + var toFlush = new List(Buffer); + Buffer.Clear(); + _bufferBytes = 0; + return toFlush; + } + + private static int EstimateSize(RecordPayload record) + { + var total = 0; + + total += GetByteCount(record.ProxyAssemblyQualifiedName); + total += GetByteCount(record.TargetAssemblyName); + total += GetByteCount(record.TargetTypeName); + total += GetByteCount(record.ParentTargetAssemblyName); + total += GetByteCount(record.ParentTargetTypeName); + + // Rough overhead for JSON quotes, commas, braces, and property names + total += 128; + + return total; + } + + private static int GetByteCount(string value) + { + if (string.IsNullOrEmpty(value)) + { + return 0; + } + + return Encoding.UTF8.GetByteCount(value); + } + + private static Type? ResolveParentType(Type targetType) + { + if (targetType.BaseType != null && targetType.BaseType != typeof(object)) + { + return targetType.BaseType; + } + + var interfaces = targetType.GetInterfaces(); + return interfaces.Length > 0 ? interfaces[0] : null; + } + + private sealed class RecordPayload + { + public string ProxyAssemblyQualifiedName { get; set; } = string.Empty; + + public string TargetAssemblyName { get; set; } = string.Empty; + + public string TargetTypeName { get; set; } = string.Empty; + + public string ParentTargetAssemblyName { get; set; } = string.Empty; + + public string ParentTargetTypeName { get; set; } = string.Empty; + } +}