Skip to content

Commit 957224c

Browse files
committed
Merge branch 'main' into rlamb/add-dotnet-singleton
2 parents d608cc7 + ee51f80 commit 957224c

File tree

19 files changed

+1187
-46
lines changed

19 files changed

+1187
-46
lines changed

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
using System.ComponentModel;
21
using System.Diagnostics;
32
using LaunchDarkly.Observability;
43
using LaunchDarkly.Sdk;
54
using LaunchDarkly.Sdk.Server;
65
using LaunchDarkly.Sdk.Server.Integrations;
7-
using Microsoft.Extensions.Logging;
86

97
var builder = WebApplication.CreateBuilder(args);
108

@@ -141,25 +139,25 @@
141139
var logMessages = new[]
142140
{
143141
"User authentication successful",
144-
"Database connection established",
142+
"Database connection established",
145143
"Cache miss occurred, falling back to database",
146144
"API rate limit approaching threshold",
147145
"Background job completed successfully"
148146
};
149-
147+
150148
var severityLevels = new[] { LogLevel.Information, LogLevel.Warning, LogLevel.Error, LogLevel.Debug };
151-
149+
152150
var message = logMessages[random.Next(logMessages.Length)];
153151
var severity = severityLevels[random.Next(severityLevels.Length)];
154-
152+
155153
Observe.RecordLog(message, severity, new Dictionary<string, object>
156154
{
157-
{"component", "asp-sample-app"},
158-
{"endpoint", "/recordlog"},
159-
{"timestamp", DateTime.UtcNow.ToString("O")},
160-
{"user_id", Guid.NewGuid().ToString()}
155+
{ "component", "asp-sample-app" },
156+
{ "endpoint", "/recordlog" },
157+
{ "timestamp", DateTime.UtcNow.ToString("O") },
158+
{ "user_id", Guid.NewGuid().ToString() }
161159
});
162-
160+
163161
return new
164162
{
165163
message = "Log recorded successfully",
@@ -190,6 +188,8 @@
190188
.WithName("GetManualInstrumentation")
191189
.WithOpenApi();
192190

191+
app.MapGet("/crash", () => { throw new NotImplementedException(); }).WithName("Crash").WithOpenApi();
192+
193193
app.Run();
194194

195195
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: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
using System.Threading;
44
using System.Threading.Tasks;
55
using LaunchDarkly.Observability.Otel;
6+
using LaunchDarkly.Logging;
7+
using LaunchDarkly.Observability.Logging;
8+
using LaunchDarkly.Observability.Sampling;
69
using Microsoft.Extensions.DependencyInjection;
710
using Microsoft.Extensions.Hosting;
811
using Microsoft.Extensions.Logging;
12+
using OpenTelemetry;
913
using OpenTelemetry.Resources;
1014
using OpenTelemetry.Trace;
1115
using OpenTelemetry.Exporter;
@@ -23,11 +27,13 @@ public static class ObservabilityExtensions
2327
private const int FlushIntervalMs = 5 * 1000;
2428
private const int MaxExportBatchSize = 10000;
2529
private const int MaxQueueSize = 10000;
30+
private const int ExportTimeoutMs = 30000;
2631

2732
private const string TracesPath = "/v1/traces";
2833
private const string LogsPath = "/v1/logs";
2934
private const string MetricsPath = "/v1/metrics";
3035

36+
3137
private class LdObservabilityHostedService : IHostedService
3238
{
3339
private readonly ObservabilityConfig _config;
@@ -48,13 +54,31 @@ public Task StartAsync(CancellationToken cancellationToken)
4854
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
4955
}
5056

57+
private static async Task GetSamplingConfigAsync(CustomSampler sampler, ObservabilityConfig config)
58+
{
59+
using (var samplingClient = new SamplingConfigClient(config.BackendUrl))
60+
{
61+
try
62+
{
63+
var res = await samplingClient.GetSamplingConfigAsync(config.SdkKey).ConfigureAwait(false);
64+
if (res == null) return;
65+
sampler.SetConfig(res);
66+
}
67+
catch (Exception ex)
68+
{
69+
DebugLogger.DebugLog($"Exception while getting sampling config: {ex}");
70+
}
71+
}
72+
}
73+
5174
private static IEnumerable<KeyValuePair<string, object>> GetResourceAttributes(ObservabilityConfig config)
5275
{
5376
var attrs = new List<KeyValuePair<string, object>>();
5477

5578
if (!string.IsNullOrWhiteSpace(config.Environment))
5679
{
57-
attrs.Add(new KeyValuePair<string, object>(AttributeNames.DeploymentEnvironment, config.Environment));
80+
attrs.Add(
81+
new KeyValuePair<string, object>(AttributeNames.DeploymentEnvironment, config.Environment));
5882
}
5983

6084
attrs.Add(new KeyValuePair<string, object>(AttributeNames.ProjectId, config.SdkKey));
@@ -63,10 +87,16 @@ private static IEnumerable<KeyValuePair<string, object>> GetResourceAttributes(O
6387
}
6488

6589
internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollection services,
66-
ObservabilityConfig config)
90+
ObservabilityConfig config, Logger logger = null)
6791
{
92+
DebugLogger.SetLogger(logger);
6893
var resourceAttributes = GetResourceAttributes(config);
6994

95+
var sampler = new CustomSampler();
96+
97+
// Asynchronously get sampling config.
98+
_ = Task.Run(() => GetSamplingConfigAsync(sampler, config));
99+
70100
var resourceBuilder = ResourceBuilder.CreateDefault();
71101
if (!string.IsNullOrWhiteSpace(config.ServiceName))
72102
{
@@ -82,19 +112,21 @@ internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollect
82112
.AddWcfInstrumentation()
83113
.AddQuartzInstrumentation()
84114
.AddAspNetCoreInstrumentation(options => { options.RecordException = true; })
85-
.AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; })
86-
.AddSource(config.ServiceName ?? DefaultNames.ActivitySourceName)
87-
.AddOtlpExporter(options =>
88-
{
89-
options.Endpoint = new Uri(config.OtlpEndpoint + TracesPath);
90-
options.Protocol = ExportProtocol;
91-
options.BatchExportProcessorOptions.MaxExportBatchSize = MaxExportBatchSize;
92-
options.BatchExportProcessorOptions.MaxQueueSize = MaxQueueSize;
93-
options.BatchExportProcessorOptions.ScheduledDelayMilliseconds = FlushIntervalMs;
94-
});
115+
.AddSqlClientInstrumentation(options => { options.SetDbStatementForText = true; });
116+
117+
// Always use sampling exporter for traces
118+
var samplingTraceExporter = new SamplingTraceExporter(sampler, new OtlpExporterOptions
119+
{
120+
Endpoint = new Uri(config.OtlpEndpoint + TracesPath),
121+
Protocol = OtlpExportProtocol.HttpProtobuf,
122+
});
123+
124+
tracing.AddProcessor(new BatchActivityExportProcessor(samplingTraceExporter, MaxQueueSize,
125+
FlushIntervalMs, ExportTimeoutMs, MaxExportBatchSize));
95126
}).WithLogging(logging =>
96127
{
97128
logging.SetResourceBuilder(resourceBuilder)
129+
.AddProcessor(new SamplingLogProcessor(sampler))
98130
.AddOtlpExporter(options =>
99131
{
100132
options.Endpoint = new Uri(config.OtlpEndpoint + LogsPath);
@@ -121,7 +153,8 @@ internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollect
121153

122154
// Attach a hosted service which will allow us to get a logger provider instance from the built
123155
// service collection.
124-
services.AddHostedService((serviceProvider) => new LdObservabilityHostedService(config, serviceProvider));
156+
services.AddHostedService((serviceProvider) =>
157+
new LdObservabilityHostedService(config, serviceProvider));
125158
}
126159

127160
/// <summary>

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)