Skip to content

Commit 1eed2db

Browse files
committed
feat: Add tracing hook.
1 parent 03e2a59 commit 1eed2db

File tree

19 files changed

+1681
-1
lines changed

19 files changed

+1681
-1
lines changed

.github/workflows/manual-publish-doc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
- libs/client-sdk
1111
- libs/server-sdk
1212
- libs/server-sdk-redis-source
13+
- libs/server-sdk-otel
1314
name: Publish Documentation
1415
jobs:
1516
build-publish:

.github/workflows/release-please.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ jobs:
1414
package-server-tag: ${{ steps.release.outputs['libs/server-sdk--tag_name'] }}
1515
package-server-redis-released: ${{ steps.release.outputs['libs/server-sdk-redis-source--release_created'] }}
1616
package-server-redis-tag: ${{ steps.release.outputs['libs/server-sdk-redis-source--tag_name'] }}
17+
package-server-otel-released: ${{ steps.release.outputs['libs/server-sdk-otel--release_created'] }}
18+
package-server-otel-tag: ${{ steps.release.outputs['libs/server-sdk-otel--tag_name'] }}
1719
steps:
1820
- uses: googleapis/release-please-action@v4
1921
id: release
@@ -142,3 +144,4 @@ jobs:
142144
upload-assets: true
143145
upload-tag-name: ${{ needs.release-please.outputs.package-server-redis-tag }}
144146
provenance-name: ${{ format('{0}-server-redis-multiple-provenance.intoto.jsonl', matrix.os) }}
147+

.github/workflows/server-otel.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: libs/server-sdk-otel
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths-ignore:
7+
- '**.md' # Do not need to run CI for markdown changes.
8+
pull_request:
9+
branches: [ "main", "feat/**" ]
10+
paths-ignore:
11+
- '**.md'
12+
schedule:
13+
# Run daily at midnight PST
14+
- cron: '0 8 * * *'
15+
16+
jobs:
17+
build-test-otel:
18+
runs-on: ubuntu-22.04
19+
steps:
20+
- uses: actions/checkout@v4
21+
- uses: ./.github/actions/ci
22+
with:
23+
cmake_target: launchdarkly-cpp-server-otel
24+
cmake_extra_options: '-DLD_BUILD_OTEL_SUPPORT=ON -DLD_BUILD_OTEL_FETCH_DEPS=ON'
25+
simulate_release: true
26+
build-otel-mac:
27+
runs-on: macos-13
28+
steps:
29+
- uses: actions/checkout@v4
30+
- uses: ./.github/actions/ci
31+
with:
32+
cmake_target: launchdarkly-cpp-server-otel
33+
cmake_extra_options: '-DLD_BUILD_OTEL_SUPPORT=ON -DLD_BUILD_OTEL_FETCH_DEPS=ON'
34+
platform_version: 12
35+
simulate_release: true
36+
build-test-otel-windows:
37+
runs-on: windows-2022
38+
steps:
39+
- uses: actions/checkout@v4
40+
- uses: ilammy/msvc-dev-cmd@v1
41+
- uses: ./.github/actions/ci
42+
env:
43+
BOOST_LIBRARY_DIR: 'C:\local\boost_1_87_0\lib64-msvc-14.3'
44+
BOOST_LIBRARYDIR: 'C:\local\boost_1_87_0\lib64-msvc-14.3'
45+
Boost_DIR: 'C:\local\boost_1_87_0\lib64-msvc-14.3\cmake\Boost-1.87.0'
46+
with:
47+
cmake_target: launchdarkly-cpp-server-otel
48+
cmake_extra_options: '-DLD_BUILD_OTEL_SUPPORT=ON -DLD_BUILD_OTEL_FETCH_DEPS=ON'
49+
platform_version: 2022
50+
toolset: msvc
51+
simulate_windows_release: true

.release-please-manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"libs/common": "1.10.0",
55
"libs/internal": "0.12.1",
66
"libs/server-sdk": "3.9.1",
7-
"libs/server-sdk-redis-source": "2.2.0"
7+
"libs/server-sdk-redis-source": "2.2.0",
8+
"libs/server-sdk-otel": "0.0.0"
89
}

CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ option(LD_BUILD_EXAMPLES "Build hello-world examples." ON)
101101

102102
option(LD_BUILD_REDIS_SUPPORT "Build redis support." OFF)
103103

104+
option(LD_BUILD_OTEL_SUPPORT "Build OpenTelemetry integration." OFF)
105+
104106
# If using 'make' as the build system, CMake causes the 'install' target to have a dependency on 'all', meaning
105107
# it will cause a full build. This disables that, allowing us to build piecemeal instead. This is useful
106108
# so that we only need to build the client or server for a given release (if only the client or server were affected.)
@@ -195,6 +197,11 @@ if (LD_BUILD_REDIS_SUPPORT)
195197
add_subdirectory(libs/server-sdk-redis-source)
196198
endif ()
197199

200+
if (LD_BUILD_OTEL_SUPPORT)
201+
message("LaunchDarkly: building OpenTelemetry integration")
202+
add_subdirectory(libs/server-sdk-otel)
203+
endif ()
204+
198205
# Built as static or shared depending on LD_BUILD_SHARED_LIBS variable.
199206
# This target "links" in common, internal, and sse as object libraries.
200207
add_subdirectory(libs/client-sdk)

examples/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ if (LD_BUILD_REDIS_SUPPORT)
88
add_subdirectory(hello-cpp-server-redis)
99
add_subdirectory(hello-c-server-redis)
1010
endif ()
11+
12+
if (LD_BUILD_OTEL_SUPPORT)
13+
add_subdirectory(hello-cpp-server-otel)
14+
endif ()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Required for Apple Silicon support.
2+
cmake_minimum_required(VERSION 3.19)
3+
4+
project(
5+
LaunchDarklyHelloCPPServerOTel
6+
VERSION 0.1
7+
DESCRIPTION "LaunchDarkly Hello CPP Server-side SDK with OpenTelemetry Integration"
8+
LANGUAGES CXX
9+
)
10+
11+
if(POLICY CMP0167)
12+
# Uses the BoostConfig.cmake included with the boost distribution.
13+
cmake_policy(SET CMP0167 NEW)
14+
endif()
15+
16+
set(THREADS_PREFER_PTHREAD_FLAG ON)
17+
find_package(Threads REQUIRED)
18+
19+
# Find Boost
20+
find_package(Boost 1.81 REQUIRED COMPONENTS system)
21+
22+
add_executable(hello-cpp-server-otel main.cpp)
23+
24+
target_link_libraries(hello-cpp-server-otel
25+
PRIVATE
26+
launchdarkly::server
27+
launchdarkly::server_otel
28+
Threads::Threads
29+
${Boost_LIBRARIES}
30+
opentelemetry_trace
31+
opentelemetry_exporter_otlp_http
32+
)
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# LaunchDarkly C++ Server SDK - OpenTelemetry Integration Example
2+
3+
This example demonstrates how to integrate the LaunchDarkly C++ Server SDK with OpenTelemetry tracing to automatically enrich your distributed traces with feature flag evaluation data.
4+
5+
## What This Example Shows
6+
7+
- Setting up OpenTelemetry with OTLP HTTP exporter
8+
- Configuring the LaunchDarkly OpenTelemetry tracing hook
9+
- Creating HTTP spans with Boost.Beast
10+
- Automatic feature flag span events in traces
11+
- Passing explicit parent span context to evaluations
12+
13+
## Prerequisites
14+
15+
- C++17 or later
16+
- CMake 3.19 or later
17+
- Boost 1.81 or later
18+
- LaunchDarkly SDK key
19+
- OpenTelemetry collector (or compatible backend) running on `localhost:4318`
20+
21+
## Building
22+
23+
From the repository root:
24+
25+
```bash
26+
mkdir build && cd build
27+
cmake .. -DLD_BUILD_EXAMPLES=ON -DLD_BUILD_OTEL_SUPPORT=ON
28+
cmake --build . --target hello-cpp-server-otel
29+
```
30+
31+
## Running
32+
33+
### 1. Start an OpenTelemetry Collector
34+
35+
The easiest way is using Docker:
36+
37+
```bash
38+
docker run -p 4318:4318 otel/opentelemetry-collector:latest
39+
```
40+
41+
Or use Jaeger (which has a built-in OTLP receiver):
42+
43+
```bash
44+
docker run -d -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest
45+
```
46+
47+
### 2. Set Your LaunchDarkly SDK Key
48+
49+
Either edit `main.cpp` and set the `SDK_KEY` constant, or use an environment variable:
50+
51+
```bash
52+
export LD_SDK_KEY=your-sdk-key-here
53+
```
54+
55+
### 3. Create a Feature Flag
56+
57+
In your LaunchDarkly dashboard, create a boolean flag named `show-detailed-weather`.
58+
59+
### 4. Run the Example
60+
61+
```bash
62+
./build/examples/hello-cpp-server-otel/hello-cpp-server-otel
63+
```
64+
65+
You should see:
66+
67+
```
68+
*** SDK successfully initialized!
69+
70+
*** Weather server running on http://0.0.0.0:8080
71+
*** Try: curl http://localhost:8080/weather
72+
*** OpenTelemetry tracing enabled (OTLP HTTP to localhost:4318)
73+
*** LaunchDarkly integration enabled with OpenTelemetry tracing hook
74+
```
75+
76+
### 5. Make Requests
77+
78+
```bash
79+
curl http://localhost:8080/weather
80+
```
81+
82+
### 6. View Traces
83+
84+
If using Jaeger, open http://localhost:16686 in your browser. You should see traces with:
85+
86+
- HTTP request spans
87+
- Feature flag evaluation events with attributes:
88+
- `feature_flag.key`: "show-detailed-weather"
89+
- `feature_flag.provider.name`: "LaunchDarkly"
90+
- `feature_flag.context.id`: Context canonical key
91+
- `feature_flag.result.value`: The flag value (since `IncludeValue` is enabled)
92+
93+
## How It Works
94+
95+
### OpenTelemetry Setup
96+
97+
```cpp
98+
void InitTracer() {
99+
opentelemetry::exporter::otlp::OtlpHttpExporterOptions opts;
100+
opts.url = "http://localhost:4318/v1/traces";
101+
102+
auto exporter = opentelemetry::exporter::otlp::OtlpHttpExporterFactory::Create(opts);
103+
auto processor = trace_sdk::SimpleSpanProcessorFactory::Create(std::move(exporter));
104+
std::shared_ptr<trace_api::TracerProvider> provider =
105+
trace_sdk::TracerProviderFactory::Create(std::move(processor));
106+
trace_api::Provider::SetTracerProvider(provider);
107+
}
108+
```
109+
110+
### LaunchDarkly Hook Setup
111+
112+
```cpp
113+
auto hook_options = launchdarkly::server_side::integrations::otel::TracingHookOptionsBuilder()
114+
.IncludeValue(true) // Include flag values in traces
115+
.CreateSpans(false) // Only create span events, not full spans
116+
.Build();
117+
auto tracing_hook = std::make_shared<launchdarkly::server_side::integrations::otel::TracingHook>(hook_options);
118+
119+
auto config = launchdarkly::server_side::ConfigBuilder(sdk_key)
120+
.Hooks(tracing_hook)
121+
.Build();
122+
```
123+
124+
### Passing Parent Span Context
125+
126+
When using async frameworks like Boost.Beast, you need to manually pass the parent span:
127+
128+
```cpp
129+
auto span = tracer->StartSpan("HTTP GET /weather");
130+
auto scope = trace_api::Scope(span);
131+
132+
// Create hook context with the span
133+
auto hook_ctx = launchdarkly::server_side::integrations::otel::MakeHookContextWithSpan(span);
134+
135+
// Pass it to the evaluation
136+
auto flag_value = ld_client->BoolVariation(context, "my-flag", false, hook_ctx);
137+
```
138+
139+
This ensures feature flag events appear as children of the correct span.
140+
141+
## What You'll See
142+
143+
### In Your Application Logs
144+
145+
```
146+
*** SDK successfully initialized!
147+
148+
*** Weather server running on http://0.0.0.0:8080
149+
```
150+
151+
### In Your Traces
152+
153+
Each HTTP request will have:
154+
1. **Root Span**: "HTTP GET /weather" with HTTP attributes
155+
2. **Span Event**: "feature_flag" with LaunchDarkly evaluation details
156+
157+
Example trace structure:
158+
```
159+
HTTP GET /weather (span)
160+
└─ feature_flag (event)
161+
├─ feature_flag.key: "show-detailed-weather"
162+
├─ feature_flag.provider.name: "LaunchDarkly"
163+
├─ feature_flag.context.id: "user:weather-api-user"
164+
└─ feature_flag.result.value: "true"
165+
```
166+
167+
## Customization
168+
169+
### Include/Exclude Flag Values
170+
171+
For privacy, you can exclude flag values from traces:
172+
173+
```cpp
174+
.IncludeValue(false) // Don't include flag values
175+
```
176+
177+
### Create Dedicated Spans
178+
179+
For detailed performance tracking:
180+
181+
```cpp
182+
.CreateSpans(true) // Create a span for each evaluation
183+
```
184+
185+
This creates spans like `LDClient.BoolVariation` in addition to the feature_flag event.
186+
187+
### Set Environment ID
188+
189+
To include environment information in traces:
190+
191+
```cpp
192+
.EnvironmentId("production")
193+
```
194+
195+
## Troubleshooting
196+
197+
### No traces appear
198+
199+
1. Verify OpenTelemetry collector is running: `curl http://localhost:4318/v1/traces`
200+
2. Check the SDK initialized successfully
201+
3. Ensure you're making requests to the server
202+
203+
### Feature flag events missing
204+
205+
1. Verify the hook is registered before creating the client
206+
2. Check that you're passing the HookContext when evaluating flags in async contexts
207+
3. Ensure there's an active span when the evaluation happens
208+
209+
## Architecture
210+
211+
This example uses:
212+
- **Boost.Beast**: Async HTTP server
213+
- **OpenTelemetry C++**: Distributed tracing
214+
- **LaunchDarkly C++ Server SDK**: Feature flags
215+
- **LaunchDarkly OTel Integration**: Automatic trace enrichment
216+
217+
The integration is non-invasive - the hook automatically captures all flag evaluations without changing your evaluation code.

0 commit comments

Comments
 (0)