Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<ItemGroup>
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.*" />
<PackageReference Include="LaunchDarkly.ServerSdk.Ai" Version="0.*" />
<PackageReference Include="LaunchDarkly.Observability" Version="1.*" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />
<PackageReference Include="OpenAI" Version="2.*" />
<PackageReference Include="DotNetEnv" Version="3.*" />
</ItemGroup>
Expand Down
36 changes: 35 additions & 1 deletion server-sdk-ai/frameworks/openai/chat-completions/Program.cs
Original file line number Diff line number Diff line change
@@ -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))
{
Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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();
}

Expand Down
2 changes: 2 additions & 0 deletions server-sdk-ai/frameworks/openai/chat-completions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions server-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
145 changes: 145 additions & 0 deletions server-sdk/features/plugins/observability/Hello.cs
Original file line number Diff line number Diff line change
@@ -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<string, object> { { "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();
}
}
}
4 changes: 4 additions & 0 deletions server-sdk/features/plugins/observability/NOTICE
Original file line number Diff line number Diff line change
@@ -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/).
15 changes: 15 additions & 0 deletions server-sdk/features/plugins/observability/Observability.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<!-- This dependency deliberately refers to 8.* rather than a specific
version, to ensure that the demo always uses the latest public
release of the SDK. -->
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.*" />
<PackageReference Include="LaunchDarkly.Observability" Version="1.*" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />
</ItemGroup>
</Project>
53 changes: 53 additions & 0 deletions server-sdk/features/plugins/observability/README.md
Original file line number Diff line number Diff line change
@@ -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 <flagKey> feature flag evaluates to <flagValue>." 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.
Loading