Skip to content

Commit c32cf1c

Browse files
authored
feat: Add singleton for dotnet. (#185)
## Summary Implements singleton for recording observability information for .Net. ## 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 4024ecc commit c32cf1c

7 files changed

Lines changed: 1630 additions & 12 deletions

File tree

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

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics;
12
using LaunchDarkly.Observability;
23
using LaunchDarkly.Sdk;
34
using LaunchDarkly.Sdk.Server;
@@ -35,6 +36,86 @@
3536
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
3637
};
3738

39+
app.MapGet("/recordexception", () =>
40+
{
41+
Observe.RecordException(new InvalidOperationException("this is a recorded exception"),
42+
new Dictionary<string, object>
43+
{
44+
{ "key", "value" },
45+
});
46+
return "";
47+
})
48+
.WithName("GetRecordException")
49+
.WithOpenApi();
50+
51+
app.MapGet("/recordmetrics", () =>
52+
{
53+
var random = new Random();
54+
55+
// Record a gauge metric (CPU usage percentage)
56+
var cpuUsage = Math.Round(random.NextDouble() * 100, 2);
57+
Observe.RecordMetric("cpu_usage_percent", cpuUsage, new Dictionary<string, object>
58+
{
59+
{ "environment", "development" },
60+
{ "service", "asp-sample" }
61+
});
62+
63+
// Record a counter with random value (requests processed)
64+
var requestsProcessed = random.Next(1, 100);
65+
Observe.RecordCount("requests_processed", requestsProcessed, new Dictionary<string, object>
66+
{
67+
{ "operation", "test" },
68+
{ "status", "success" }
69+
});
70+
71+
// Record an increment (counter with value 1)
72+
Observe.RecordIncr("endpoint_hits", new Dictionary<string, object>
73+
{
74+
{ "endpoint", "/recordmetrics" },
75+
{ "method", "GET" }
76+
});
77+
78+
// Record a histogram value (request duration in seconds)
79+
var requestDuration = Math.Round(random.NextDouble() * 2.0, 3); // 0-2 seconds
80+
Observe.RecordHistogram("request_duration_seconds", requestDuration, new Dictionary<string, object>
81+
{
82+
{ "handler", "recordmetrics" },
83+
{ "response_code", "200" }
84+
});
85+
86+
// Record an up-down counter (active connections - positive)
87+
var connectionDelta = random.Next(1, 10);
88+
Observe.RecordUpDownCounter("active_connections", connectionDelta, new Dictionary<string, object>
89+
{
90+
{ "connection_type", "http" },
91+
{ "region", "us-east-1" }
92+
});
93+
94+
// Record another up-down counter with negative delta (queue items processed)
95+
var queueDelta = -random.Next(1, 5);
96+
Observe.RecordUpDownCounter("queue_size", queueDelta, new Dictionary<string, object>
97+
{
98+
{ "queue_name", "processing" },
99+
{ "priority", "high" }
100+
});
101+
102+
return new
103+
{
104+
message = "Metrics recorded successfully",
105+
metrics = new
106+
{
107+
gauge = $"cpu_usage_percent: {cpuUsage}%",
108+
counter = $"requests_processed: +{requestsProcessed}",
109+
increment = "endpoint_hits: +1",
110+
histogram = $"request_duration_seconds: {requestDuration}s",
111+
upDownCounter1 = $"active_connections: +{connectionDelta}",
112+
upDownCounter2 = $"queue_size: {queueDelta}"
113+
}
114+
};
115+
})
116+
.WithName("GetRecordMetrics")
117+
.WithOpenApi();
118+
38119
app.MapGet("/weatherforecast", () =>
39120
{
40121
var isMercury =
@@ -52,10 +133,62 @@
52133
.WithName("GetWeatherForecast")
53134
.WithOpenApi();
54135

55-
app.MapGet("/crash", () =>
56-
{
57-
throw new NotImplementedException();
58-
}).WithName("Crash").WithOpenApi();
136+
app.MapGet("/recordlog", () =>
137+
{
138+
var random = new Random();
139+
var logMessages = new[]
140+
{
141+
"User authentication successful",
142+
"Database connection established",
143+
"Cache miss occurred, falling back to database",
144+
"API rate limit approaching threshold",
145+
"Background job completed successfully"
146+
};
147+
148+
var severityLevels = new[] { LogLevel.Information, LogLevel.Warning, LogLevel.Error, LogLevel.Debug };
149+
150+
var message = logMessages[random.Next(logMessages.Length)];
151+
var severity = severityLevels[random.Next(severityLevels.Length)];
152+
153+
Observe.RecordLog(message, severity, new Dictionary<string, object>
154+
{
155+
{ "component", "asp-sample-app" },
156+
{ "endpoint", "/recordlog" },
157+
{ "timestamp", DateTime.UtcNow.ToString("O") },
158+
{ "user_id", Guid.NewGuid().ToString() }
159+
});
160+
161+
return new
162+
{
163+
message = "Log recorded successfully",
164+
log_entry = new
165+
{
166+
message,
167+
severity = severity.ToString(),
168+
component = "asp-sample-app"
169+
}
170+
};
171+
})
172+
.WithName("GetRecordLog")
173+
.WithOpenApi();
174+
175+
app.MapGet("/manualinstrumentation", () =>
176+
{
177+
using (Observe.StartActivity("manual-instrumentation", ActivityKind.Internal,
178+
new Dictionary<string, object> { { "test", "attribute" } }))
179+
{
180+
var enableMetrics = client.BoolVariation("enableMetrics",
181+
Context.New(ContextKind.Of("request"), Guid.NewGuid().ToString()));
182+
183+
if (!enableMetrics) return "Manual instrumentation completed";
184+
Observe.RecordIncr("manual_instrumentation_calls");
185+
return "Manual instrumentation completed with metrics enabled";
186+
}
187+
})
188+
.WithName("GetManualInstrumentation")
189+
.WithOpenApi();
190+
191+
app.MapGet("/crash", () => { throw new NotImplementedException(); }).WithName("Crash").WithOpenApi();
59192

60193
app.Run();
61194

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace LaunchDarkly.Observability
2+
{
3+
internal static class DefaultNames
4+
{
5+
public const string MeterName = "launchdarkly-plugin-default-metrics";
6+
public const string ActivitySourceName = "launchdarkly-plugin-default-activity";
7+
public const string DefaultLoggerName = "launchdarkly-plugin-default-logger";
8+
}
9+
}

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

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Diagnostics;
3+
using System.Threading;
44
using System.Threading.Tasks;
5+
using LaunchDarkly.Observability.Otel;
56
using LaunchDarkly.Logging;
67
using LaunchDarkly.Observability.Logging;
7-
using LaunchDarkly.Observability.Otel;
88
using LaunchDarkly.Observability.Sampling;
99
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Hosting;
11+
using Microsoft.Extensions.Logging;
12+
using OpenTelemetry;
1013
using OpenTelemetry.Resources;
1114
using OpenTelemetry.Trace;
12-
using OpenTelemetry;
1315
using OpenTelemetry.Exporter;
1416
using OpenTelemetry.Logs;
1517
using OpenTelemetry.Metrics;
@@ -21,8 +23,6 @@ namespace LaunchDarkly.Observability
2123
/// </summary>
2224
public static class ObservabilityExtensions
2325
{
24-
// Used for metrics when a service name is not specified.
25-
private const string DefaultMetricsName = "launchdarkly-plugin-default-metrics";
2626
private const OtlpExportProtocol ExportProtocol = OtlpExportProtocol.HttpProtobuf;
2727
private const int FlushIntervalMs = 5 * 1000;
2828
private const int MaxExportBatchSize = 10000;
@@ -33,6 +33,27 @@ public static class ObservabilityExtensions
3333
private const string LogsPath = "/v1/logs";
3434
private const string MetricsPath = "/v1/metrics";
3535

36+
37+
private class LdObservabilityHostedService : IHostedService
38+
{
39+
private readonly ObservabilityConfig _config;
40+
private readonly ILoggerProvider _loggerProvider;
41+
42+
public LdObservabilityHostedService(ObservabilityConfig config, IServiceProvider provider)
43+
{
44+
_loggerProvider = provider.GetService<ILoggerProvider>();
45+
_config = config;
46+
}
47+
48+
public Task StartAsync(CancellationToken cancellationToken)
49+
{
50+
Observe.Initialize(_config, _loggerProvider);
51+
return Task.CompletedTask;
52+
}
53+
54+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
55+
}
56+
3657
private static async Task GetSamplingConfigAsync(CustomSampler sampler, ObservabilityConfig config)
3758
{
3859
using (var samplingClient = new SamplingConfigClient(config.BackendUrl))
@@ -56,10 +77,11 @@ private static IEnumerable<KeyValuePair<string, object>> GetResourceAttributes(O
5677

5778
if (!string.IsNullOrWhiteSpace(config.Environment))
5879
{
59-
attrs.Add(new KeyValuePair<string, object>("deployment.environment.name", config.Environment));
80+
attrs.Add(
81+
new KeyValuePair<string, object>(AttributeNames.DeploymentEnvironment, config.Environment));
6082
}
6183

62-
attrs.Add(new KeyValuePair<string, object>("highlight.project_id", config.SdkKey));
84+
attrs.Add(new KeyValuePair<string, object>(AttributeNames.ProjectId, config.SdkKey));
6385

6486
return attrs;
6587
}
@@ -116,7 +138,7 @@ internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollect
116138
}).WithMetrics(metrics =>
117139
{
118140
metrics.SetResourceBuilder(resourceBuilder)
119-
.AddMeter(config.ServiceName ?? DefaultMetricsName)
141+
.AddMeter(config.ServiceName ?? DefaultNames.MeterName)
120142
.AddRuntimeInstrumentation()
121143
.AddProcessInstrumentation()
122144
.AddHttpClientInstrumentation()
@@ -128,6 +150,11 @@ internal static void AddLaunchDarklyObservabilityWithConfig(this IServiceCollect
128150
Protocol = ExportProtocol
129151
})));
130152
});
153+
154+
// Attach a hosted service which will allow us to get a logger provider instance from the built
155+
// service collection.
156+
services.AddHostedService((serviceProvider) =>
157+
new LdObservabilityHostedService(config, serviceProvider));
131158
}
132159

133160
/// <summary>

0 commit comments

Comments
 (0)