diff --git a/server-sdk-ai/frameworks/openai/chat-completions/.env.example b/server-sdk-ai/frameworks/openai/chat-completions/.env.example
index f7dc420..d1d26bf 100644
--- a/server-sdk-ai/frameworks/openai/chat-completions/.env.example
+++ b/server-sdk-ai/frameworks/openai/chat-completions/.env.example
@@ -6,3 +6,8 @@ OPENAI_API_KEY=
# Override to use a different AI Config key
LAUNCHDARKLY_COMPLETION_KEY=sample-completion
+
+# The example enables the OpenAI SDK's experimental OpenTelemetry instrumentation
+# in code. Optionally override the observability plugin's OTLP endpoint (for
+# example on networks that only allow port 443).
+# OTEL_EXPORTER_OTLP_ENDPOINT=
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj b/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj
index 7c15241..fdf6503 100644
--- a/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj
+++ b/server-sdk-ai/frameworks/openai/chat-completions/OpenAiChatCompletions.csproj
@@ -10,6 +10,8 @@
+
+
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/Program.cs b/server-sdk-ai/frameworks/openai/chat-completions/Program.cs
index 6664a47..1a4d8b2 100644
--- a/server-sdk-ai/frameworks/openai/chat-completions/Program.cs
+++ b/server-sdk-ai/frameworks/openai/chat-completions/Program.cs
@@ -1,14 +1,25 @@
using DotNetEnv;
+using LaunchDarkly.Observability;
using LaunchDarkly.Sdk;
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Ai;
using LaunchDarkly.Sdk.Server.Ai.Adapters;
using LaunchDarkly.Sdk.Server.Ai.Config;
using LaunchDarkly.Sdk.Server.Ai.Tracking;
+using LaunchDarkly.Sdk.Server.Integrations;
+using Microsoft.Extensions.Hosting;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
using OpenAI.Chat;
Env.TraversePath().Load();
+// Enable the OpenAI SDK's experimental OpenTelemetry instrumentation so its chat
+// completions emit spans and metrics. This must be set before the ChatClient is
+// used; the observability plugin configured below exports the telemetry to
+// LaunchDarkly.
+Environment.SetEnvironmentVariable("OPENAI_EXPERIMENTAL_ENABLE_OPEN_TELEMETRY", "true");
+
var sdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY");
if (string.IsNullOrEmpty(sdkKey))
{
@@ -29,7 +40,22 @@
var completionKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_COMPLETION_KEY")
?? "sample-completion";
-var ldClient = new LdClient(Configuration.Builder(sdkKey).Build());
+// The observability plugin registers OpenTelemetry into a dependency-injection
+// service collection and relies on the .NET generic host to run the exporters.
+var hostBuilder = Host.CreateApplicationBuilder(args);
+
+var ldClient = new LdClient(Configuration.Builder(sdkKey)
+ .Plugins(new PluginConfigurationBuilder()
+ .Add(ObservabilityPlugin.Builder(hostBuilder.Services)
+ .WithServiceName("openai-chat-completions")
+ .WithServiceVersion("1.0.0")
+ // The OpenAI SDK emits telemetry under the "OpenAI.ChatClient" activity
+ // source and meter. Add them so the plugin exports the model call's
+ // spans and token-usage metrics alongside the AI Config tracker events.
+ .WithExtendedTracingConfig(tracing => tracing.AddSource("OpenAI.ChatClient"))
+ .WithExtendedMeterConfiguration(metrics => metrics.AddMeter("OpenAI.ChatClient"))
+ .Build()))
+ .Build());
if (!ldClient.Initialized)
{
Console.Error.WriteLine(
@@ -39,6 +65,11 @@
}
Console.WriteLine("*** SDK successfully initialized!");
+// Building the LdClient registered the plugin's OpenTelemetry services on the host,
+// so the host must be built after the client. Starting it boots the exporters.
+using var host = hostBuilder.Build();
+await host.StartAsync();
+
var aiClient = new LdAiClient(new LdClientAdapter(ldClient));
// Set up the evaluation context. This context should appear on your
@@ -113,6 +144,9 @@
finally
{
ldClient.FlushAndWait(TimeSpan.FromSeconds(5));
+ // Stop the host so the OpenTelemetry exporters flush any buffered spans and
+ // metrics to LaunchDarkly before the process exits.
+ await host.StopAsync();
ldClient.Dispose();
}
diff --git a/server-sdk-ai/frameworks/openai/chat-completions/README.md b/server-sdk-ai/frameworks/openai/chat-completions/README.md
index 9375d0f..b29b774 100644
--- a/server-sdk-ai/frameworks/openai/chat-completions/README.md
+++ b/server-sdk-ai/frameworks/openai/chat-completions/README.md
@@ -2,6 +2,8 @@
This example demonstrates how to use the LaunchDarkly AI SDK for .NET with the [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat).
+It also wires up the LaunchDarkly [observability plugin](https://launchdarkly.com/docs/sdk/observability/dotnet). The example sets the `OPENAI_EXPERIMENTAL_ENABLE_OPEN_TELEMETRY` environment variable to `true` so the OpenAI SDK emits OpenTelemetry spans and metrics (under the `OpenAI.ChatClient` source and meter), and registers those with the plugin so the model call's traces and token-usage metrics are exported to LaunchDarkly alongside the AI Config tracker events.
+
## Prerequisites
- .NET 8.0 SDK
diff --git a/server-sdk/README.md b/server-sdk/README.md
index 4a84f9e..4dfc8eb 100644
--- a/server-sdk/README.md
+++ b/server-sdk/README.md
@@ -9,3 +9,9 @@ For more comprehensive instructions, you can visit your [Quickstart page](https:
| Example | Description |
|--------------------------------------|-------------------------------------------------|
| [Flag Retrieval](./getting-started/) | Initialize the SDK and evaluate a feature flag |
+
+## Features
+
+| Example | Description |
+|------------------------------------------------------|----------------------------------------------------------------------|
+| [Observability](./features/plugins/observability/) | Register the observability plugin and record custom telemetry |
diff --git a/server-sdk/features/plugins/observability/Hello.cs b/server-sdk/features/plugins/observability/Hello.cs
new file mode 100644
index 0000000..c989904
--- /dev/null
+++ b/server-sdk/features/plugins/observability/Hello.cs
@@ -0,0 +1,145 @@
+using System;
+using System.Collections.Generic;
+using LaunchDarkly.Observability;
+using LaunchDarkly.Sdk;
+using LaunchDarkly.Sdk.Server;
+using LaunchDarkly.Sdk.Server.Integrations;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace HelloDotNet
+{
+ class Hello
+ {
+ public static void ShowBanner(){
+ Console.WriteLine(
+@" ██
+ ██
+ ████████
+ ███████
+██ LAUNCHDARKLY █
+ ███████
+ ████████
+ ██
+ ██
+");
+ }
+
+ static void Main(string[] args)
+ {
+ bool CI = Environment.GetEnvironmentVariable("CI") != null;
+
+ string SdkKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_SDK_KEY");
+
+ // Set FeatureFlagKey to the feature flag key you want to evaluate.
+ string FeatureFlagKey = "sample-feature";
+
+ if (string.IsNullOrEmpty(SdkKey))
+ {
+ Console.WriteLine("*** Please set LAUNCHDARKLY_SDK_KEY environment variable to your LaunchDarkly SDK key first\n");
+ Environment.Exit(1);
+ }
+
+ // The observability plugin registers OpenTelemetry into a dependency-injection
+ // service collection and relies on the .NET generic host to run the exporters.
+ // A console application creates that host explicitly; an ASP.NET Core app would
+ // pass its existing builder.Services instead.
+ var hostBuilder = Host.CreateApplicationBuilder(args);
+
+ // Compared to the getting-started example, the only configuration change is
+ // adding the observability plugin. By default it exports telemetry to
+ // LaunchDarkly; override the endpoint with the OTEL_EXPORTER_OTLP_ENDPOINT
+ // environment variable for networks that only allow port 443.
+ var ldConfig = Configuration.Builder(SdkKey)
+ .Plugins(new PluginConfigurationBuilder()
+ .Add(ObservabilityPlugin.Builder(hostBuilder.Services)
+ .WithServiceName("hello-dotnet-observability")
+ .WithServiceVersion("1.0.0")
+ .Build()))
+ .Build();
+
+ var client = new LdClient(ldConfig);
+
+ if (client.Initialized)
+ {
+ Console.WriteLine("*** SDK successfully initialized!\n");
+ }
+ else
+ {
+ Console.WriteLine("*** SDK failed to initialize\n");
+ Environment.Exit(1);
+ }
+
+ // Building the LdClient above registered the plugin's OpenTelemetry services on
+ // the host, so the host must be built after the client. Starting it boots the
+ // exporters that ship telemetry to LaunchDarkly.
+ var host = hostBuilder.Build();
+ host.Start();
+
+ // Set up the evaluation context. This context should appear on your LaunchDarkly contexts
+ // dashboard soon after you run the demo.
+ var context = Context.Builder("example-user-key")
+ .Name("Sandy")
+ .Build();
+
+ if (Environment.GetEnvironmentVariable("LAUNCHDARKLY_FLAG_KEY") != null)
+ {
+ FeatureFlagKey = Environment.GetEnvironmentVariable("LAUNCHDARKLY_FLAG_KEY");
+ }
+
+ var flagAttributes = new Dictionary { { "flag.key", FeatureFlagKey } };
+
+ // A web framework would create spans automatically for incoming requests; a console
+ // application has none, so we start one manually with Observe.StartActivity. The custom
+ // metric and log recorded inside it are grouped under this trace in LaunchDarkly.
+ using (Observe.StartActivity("evaluate-flag"))
+ {
+ var flagValue = client.BoolVariation(FeatureFlagKey, context, false);
+
+ Console.WriteLine($"*** The {FeatureFlagKey} feature flag evaluates to {flagValue}.\n");
+
+ Observe.RecordIncr("flag_evaluations", flagAttributes);
+ Observe.RecordLog($"Evaluated {FeatureFlagKey}: {flagValue}", LogLevel.Information, flagAttributes);
+
+ if (flagValue) ShowBanner();
+ }
+
+ client.FlagTracker.FlagChanged += client.FlagTracker.FlagValueChangeHandler(
+ FeatureFlagKey,
+ context,
+ (sender, changeArgs) => {
+ // Record each change as its own span so the update is visible in LaunchDarkly.
+ using (Observe.StartActivity("flag-changed"))
+ {
+ Console.WriteLine($"*** The {FeatureFlagKey} feature flag evaluates to {changeArgs.NewValue}.\n");
+
+ Observe.RecordIncr("flag_changes", flagAttributes);
+
+ if (changeArgs.NewValue.AsBool) ShowBanner();
+ }
+ }
+ );
+
+ if (CI)
+ {
+ // In CI just verify startup, then stop the host so any buffered
+ // telemetry is flushed before exiting.
+ host.StopAsync().GetAwaiter().GetResult();
+ client.Dispose();
+ return;
+ }
+
+ Console.WriteLine("*** Waiting for changes (press Ctrl+C to exit) \n");
+
+ // Block until Ctrl+C. The host's console lifetime handles the signal,
+ // stops the OpenTelemetry exporters (flushing buffered telemetry), and
+ // then returns so the process exits cleanly. Blocking any other way
+ // (for example on a Task that never completes) would leave the process
+ // hung at "Application is shutting down..." because nothing would be
+ // driving the host's shutdown.
+ host.WaitForShutdown();
+
+ client.Dispose();
+ }
+ }
+}
diff --git a/server-sdk/features/plugins/observability/NOTICE b/server-sdk/features/plugins/observability/NOTICE
new file mode 100644
index 0000000..9583678
--- /dev/null
+++ b/server-sdk/features/plugins/observability/NOTICE
@@ -0,0 +1,4 @@
+LaunchDarkly .NET Server SDK Observability Plugin example
+Copyright 2026 Catamorphic, Co.
+
+This product includes software developed at LaunchDarkly (https://launchdarkly.com/).
diff --git a/server-sdk/features/plugins/observability/Observability.csproj b/server-sdk/features/plugins/observability/Observability.csproj
new file mode 100644
index 0000000..462e88e
--- /dev/null
+++ b/server-sdk/features/plugins/observability/Observability.csproj
@@ -0,0 +1,15 @@
+
+
+ Exe
+ net8.0
+
+
+
+
+
+
+
+
+
diff --git a/server-sdk/features/plugins/observability/README.md b/server-sdk/features/plugins/observability/README.md
new file mode 100644
index 0000000..7edbb29
--- /dev/null
+++ b/server-sdk/features/plugins/observability/README.md
@@ -0,0 +1,53 @@
+# Observability Plugin
+
+This example builds on the [Hello .NET getting-started example](../../../getting-started/) and adds the LaunchDarkly [observability plugin](https://launchdarkly.com/docs/sdk/observability/dotnet). The only change to the flag-evaluation code is registering the plugin on the SDK configuration; everything else demonstrates how to record custom telemetry from a console application.
+
+It demonstrates:
+
+- Registering `ObservabilityPlugin` on the SDK configuration with `PluginConfigurationBuilder`
+- Hosting the plugin's OpenTelemetry exporters in a console app with `Host.CreateApplicationBuilder`
+- Creating spans manually with `Observe.StartActivity`
+- Recording a custom metric with `Observe.RecordIncr`
+- Emitting a structured log with `Observe.RecordLog`
+
+## Spans in a console application
+
+Framework integrations (for example ASP.NET Core) create spans automatically for
+incoming requests. A console application has no framework generating requests, so
+**nothing produces spans unless you create them yourself** with `Observe.StartActivity`.
+This example wraps the flag evaluation, and each flag change, in a manually created
+span so the custom metrics and logs recorded inside them are grouped under a trace in
+LaunchDarkly.
+
+The plugin also relies on the .NET generic host to run its OpenTelemetry exporters.
+This console app creates one with `Host.CreateApplicationBuilder` and calls `host.Start()`
+after the `LdClient` is built (constructing the client registers the exporter services on
+the host). Because the app then stays running, the exporters flush telemetry on their
+normal schedule.
+
+## Prerequisites
+
+- .NET 8.0 SDK
+- A LaunchDarkly account and a server-side SDK key
+- [Observability enabled](https://launchdarkly.com/docs/sdk/observability/dotnet) for your LaunchDarkly project
+
+## Build instructions
+
+1. Set the environment variable `LAUNCHDARKLY_SDK_KEY` to your LaunchDarkly SDK key. If there is an existing boolean feature flag in your LaunchDarkly project that you want to evaluate, set `LAUNCHDARKLY_FLAG_KEY` to the flag key; otherwise, a boolean flag of `sample-feature` will be assumed.
+
+ ```bash
+ export LAUNCHDARKLY_SDK_KEY="1234567890abcdef"
+ export LAUNCHDARKLY_FLAG_KEY="my-boolean-flag"
+ ```
+
+ By default the plugin exports telemetry to LaunchDarkly. To send it elsewhere (for example on networks that only allow port 443), set `OTEL_EXPORTER_OTLP_ENDPOINT`.
+
+2. Run the application from the command line:
+
+ ```bash
+ dotnet run
+ ```
+
+You should receive the message "The feature flag evaluates to ." The application runs continuously and reacts to flag changes in LaunchDarkly, recording a span, a metric, and a log for each evaluation. The telemetry appears on your LaunchDarkly observability dashboard shortly afterward.
+
+> **Note:** Traces and logs are exported within a few seconds, but metrics are collected by a periodic reader and export on a longer interval (roughly a minute), so they take longer to appear. Keep the application running long enough for the export to occur.