Skip to content

Commit ee51f80

Browse files
authored
feat: Add graphql query to get sampling config. (#181)
## Summary Uses the sampling tace and log exporter with the sampling configuration. The approach used for GraphQL is a bit different on this one. The types are not tool generated, so they would require updating, but it does remove any runtime dependency. I may consider back-porting this approach to reduce dependencies in the other plugins. ## How did you test this change? Unit tests. Manual testing. ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? -->
1 parent a3d6fa7 commit ee51f80

File tree

13 files changed

+1148
-14
lines changed

13 files changed

+1148
-14
lines changed

sdk/@launchdarkly/observability-dotnet/AspSampleApp/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@
5252
.WithName("GetWeatherForecast")
5353
.WithOpenApi();
5454

55+
app.MapGet("/crash", () =>
56+
{
57+
throw new NotImplementedException();
58+
}).WithName("Crash").WithOpenApi();
59+
5560
app.Run();
5661

5762
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)

sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/LaunchDarkly.Observability.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
<!-- Allow the testing project to access internals. -->
2929
<ItemGroup Condition="'$(Configuration)'!='Release'">
3030
<InternalsVisibleTo Include="LaunchDarkly.Observability.Tests" />
31+
<!-- Dynamically generated assembly used by Moq. Required for mocking in unit tests. -->
32+
<InternalsVisibleTo Include="DynamicProxyGenAssembly2"/>
3133
</ItemGroup>
3234

3335
<PropertyGroup Condition="'$(Configuration)'=='Release' And '$(SKIP_SIGNING)'!='true'">
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Threading;
2+
using LaunchDarkly.Logging;
3+
4+
namespace LaunchDarkly.Observability.Logging
5+
{
6+
internal static class DebugLogger
7+
{
8+
private static Logger _logger;
9+
10+
/// <summary>
11+
/// Set
12+
/// </summary>
13+
/// <param name="logger"></param>
14+
public static void SetLogger(Logger logger)
15+
{
16+
if (logger == null)
17+
{
18+
Volatile.Write(ref _logger, null);
19+
return;
20+
}
21+
22+
Volatile.Write(ref _logger, logger.SubLogger("LaunchDarklyObservability"));
23+
}
24+
25+
public static void DebugLog(string message)
26+
{
27+
Volatile.Read(ref _logger)?.Debug(message);
28+
}
29+
}
30+
}

sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityExtensions.cs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Threading.Tasks;
5+
using LaunchDarkly.Logging;
6+
using LaunchDarkly.Observability.Logging;
7+
using LaunchDarkly.Observability.Otel;
8+
using LaunchDarkly.Observability.Sampling;
39
using Microsoft.Extensions.DependencyInjection;
410
using OpenTelemetry.Resources;
511
using OpenTelemetry.Trace;
@@ -21,11 +27,29 @@ public static class ObservabilityExtensions
2127
private const int FlushIntervalMs = 5 * 1000;
2228
private const int MaxExportBatchSize = 10000;
2329
private const int MaxQueueSize = 10000;
30+
private const int ExportTimeoutMs = 30000;
2431

2532
private const string TracesPath = "/v1/traces";
2633
private const string LogsPath = "/v1/logs";
2734
private const string MetricsPath = "/v1/metrics";
2835

36+
private static async Task GetSamplingConfigAsync(CustomSampler sampler, ObservabilityConfig config)
37+
{
38+
using (var samplingClient = new SamplingConfigClient(config.BackendUrl))
39+
{
40+
try
41+
{
42+
var res = await samplingClient.GetSamplingConfigAsync(config.SdkKey).ConfigureAwait(false);
43+
if (res == null) return;
44+
sampler.SetConfig(res);
45+
}
46+
catch (Exception ex)
47+
{
48+
DebugLogger.DebugLog($"Exception while getting sampling config: {ex}");
49+
}
50+
}
51+
}
52+
2953
private static IEnumerable<KeyValuePair<string, object>> GetResourceAttributes(ObservabilityConfig config)
3054
{
3155
var attrs = new List<KeyValuePair<string, object>>();
@@ -41,10 +65,16 @@ private static IEnumerable<KeyValuePair<string, object>> GetResourceAttributes(O
4165
}
4266

4367
internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollection services,
44-
ObservabilityConfig config)
68+
ObservabilityConfig config, Logger logger = null)
4569
{
70+
DebugLogger.SetLogger(logger);
4671
var resourceAttributes = GetResourceAttributes(config);
4772

73+
var sampler = new CustomSampler();
74+
75+
// Asynchronously get sampling config.
76+
_ = Task.Run(() => GetSamplingConfigAsync(sampler, config));
77+
4878
var resourceBuilder = ResourceBuilder.CreateDefault();
4979
if (!string.IsNullOrWhiteSpace(config.ServiceName))
5080
{
@@ -60,18 +90,21 @@ internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollect
6090
.AddWcfInstrumentation()
6191
.AddQuartzInstrumentation()
6292
.AddAspNetCoreInstrumentation(options => { options.RecordException = true; })
63-
.AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; })
64-
.AddOtlpExporter(options =>
65-
{
66-
options.Endpoint = new Uri(config.OtlpEndpoint + TracesPath);
67-
options.Protocol = ExportProtocol;
68-
options.BatchExportProcessorOptions.MaxExportBatchSize = MaxExportBatchSize;
69-
options.BatchExportProcessorOptions.MaxQueueSize = MaxQueueSize;
70-
options.BatchExportProcessorOptions.ScheduledDelayMilliseconds = FlushIntervalMs;
71-
});
93+
.AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; });
94+
95+
// Always use sampling exporter for traces
96+
var samplingTraceExporter = new SamplingTraceExporter(sampler, new OtlpExporterOptions
97+
{
98+
Endpoint = new Uri(config.OtlpEndpoint + TracesPath),
99+
Protocol = OtlpExportProtocol.HttpProtobuf,
100+
});
101+
102+
tracing.AddProcessor(new BatchActivityExportProcessor(samplingTraceExporter, MaxQueueSize,
103+
FlushIntervalMs, ExportTimeoutMs, MaxExportBatchSize));
72104
}).WithLogging(logging =>
73105
{
74106
logging.SetResourceBuilder(resourceBuilder)
107+
.AddProcessor(new SamplingLogProcessor(sampler))
75108
.AddOtlpExporter(options =>
76109
{
77110
options.Endpoint = new Uri(config.OtlpEndpoint + LogsPath);

sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/ObservabilityPlugin.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public override void Register(ILdClient client, EnvironmentMetadata metadata)
7373
{
7474
if (_services == null || _config == null) return;
7575
var config = _config.BuildConfig(metadata.Credential);
76-
_services.AddLaunchDarklyObservabilityWithConfig(config);
76+
_services.AddLaunchDarklyObservabilityWithConfig(config, client.GetLogger());
7777
}
7878

7979
/// <inheritdoc />

sdk/@launchdarkly/observability-dotnet/src/LaunchDarkly.Observability/Sampling/CustomSampler.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal class CustomSampler : IExportSampler
5050
private readonly SamplerFunc _sampler;
5151
private readonly ThreadSafeConfig _config = new ThreadSafeConfig();
5252
private readonly ConcurrentDictionary<string, Regex> _regexCache = new ConcurrentDictionary<string, Regex>();
53-
53+
5454
private const string SamplingRatioAttribute = "launchdarkly.sampling.ratio";
5555

5656
/// <summary>
@@ -246,7 +246,7 @@ private bool MatchesSpanConfig(SamplingConfig.SpanSamplingConfig config, Activit
246246
public SamplingResult SampleSpan(Activity span)
247247
{
248248
var config = _config.GetSamplingConfig();
249-
if (!(config?.Spans.Count > 0)) return new SamplingResult { Sample = true };
249+
if (!(config?.Spans?.Count > 0)) return new SamplingResult { Sample = true };
250250
foreach (var spanConfig in config.Spans)
251251
{
252252
if (MatchesSpanConfig(spanConfig, span))
@@ -297,7 +297,7 @@ private bool MatchesLogConfig(SamplingConfig.LogSamplingConfig config, LogRecord
297297
public SamplingResult SampleLog(LogRecord record)
298298
{
299299
var config = _config.GetSamplingConfig();
300-
if (!(config?.Logs.Count > 0)) return new SamplingResult { Sample = true };
300+
if (!(config?.Logs?.Count > 0)) return new SamplingResult { Sample = true };
301301
foreach (var logConfig in config.Logs)
302302
{
303303
if (MatchesLogConfig(logConfig, record))
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Net.Http;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace LaunchDarkly.Observability.Sampling
6+
{
7+
/// <summary>
8+
/// Minimal wrapper around HttpClient that implements IHttpClient
9+
/// </summary>
10+
internal class HttpClientWrapper : IHttpClient, System.IDisposable
11+
{
12+
private readonly HttpClient _httpClient;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the HttpClientWrapper
16+
/// </summary>
17+
/// <param name="httpClient">The HttpClient instance to wrap</param>
18+
public HttpClientWrapper(HttpClient httpClient)
19+
{
20+
_httpClient = httpClient ?? throw new System.ArgumentNullException(nameof(httpClient));
21+
}
22+
23+
/// <inheritdoc />
24+
public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content,
25+
CancellationToken cancellationToken = default)
26+
{
27+
return _httpClient.PostAsync(requestUri, content, cancellationToken);
28+
}
29+
30+
/// <summary>
31+
/// Releases all resources used by the HttpClientWrapper
32+
/// </summary>
33+
public void Dispose()
34+
{
35+
_httpClient?.Dispose();
36+
}
37+
}
38+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Net.Http;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace LaunchDarkly.Observability.Sampling
6+
{
7+
/// <summary>
8+
/// Interface for HTTP client operations used by SamplingConfigClient
9+
/// </summary>
10+
internal interface IHttpClient
11+
{
12+
/// <summary>
13+
/// Sends a POST request to the specified URI with the provided content
14+
/// </summary>
15+
/// <param name="requestUri">The URI to send the request to</param>
16+
/// <param name="content">The HTTP content to send</param>
17+
/// <param name="cancellationToken">Cancellation token for the operation</param>
18+
/// <returns>The HTTP response message</returns>
19+
Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content,
20+
CancellationToken cancellationToken = default);
21+
}
22+
}

0 commit comments

Comments
 (0)