Skip to content

Commit ddd6d4b

Browse files
authored
feat: Support Go observability initialization independent of plugins. (#230)
## Summary Allow for initialization of Go observability independent of the LaunchDarkly SDK. This allows for advanced use-cases where the LD SDK may not be in use, or may not be readily accessible. I called it `PreInitialize`, but I am not attached to that name. You do this: ```go // Initialize the observability plugin ahead of the LaunchDarkly client. // This is generally only required for advanced use cases and is not the // recommended way to initialize observability. // Observability can also be used without the LaunchDarkly client at all // by using this method. Some features will not be available when using // observability without the LaunchDarkly client. ldobserve.PreInitialize(os.Getenv("LAUNCHDARKLY_SDK_KEY"), ldobserve.WithEnvironment("test"), ldobserve.WithServiceName("go-plugin-example"), ldobserve.WithServiceVersion(version.Commit), ) ``` And now observability it setup. You can then later optionally use the plugin to facilitate registering hooks. ``` client, _ := ld.MakeCustomClient(os.Getenv("LAUNCHDARKLY_SDK_KEY"), ld.Config{ Plugins: []ldplugins.Plugin{ // This special form of constructing the observability plugin // is used when observability has been pre-initialized. ldobserve.NewObservabilityPluginWithoutInit(), }, }, 5*time.Second) ``` Or you can directly use the ldotel hook instead. Using the plugin will be more resilient in case we add more hooks. ## How did you test this change? Manual testing.
1 parent 4f266a6 commit ddd6d4b

File tree

4 files changed

+254
-77
lines changed

4 files changed

+254
-77
lines changed

e2e/go-plugin/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ This project contains several examples, each demonstrating the LaunchDarkly Obse
2929
- [Gorilla Mux Example](#3-gorilla-mux-example-cmdgorillamux)
3030
- [Standard HTTP Example](#4-standard-http-example-cmdhttp)
3131
- [Logrus Example](#5-logrus-example-cmdlogrus)
32+
- [Pre-Initialize Example](#6-pre-initialize-example-cmdpreinit)
3233

3334
### 1. Fiber Example (`cmd/fiber/`)
3435

@@ -117,4 +118,29 @@ go run cmd/logrus/logrus.go
117118

118119
**Endpoints:**
119120
- `GET /log` - Logs structured data with various field types and demonstrates Logrus + OpenTelemetry integration
121+
122+
### 6. Pre-Initialize Example (`cmd/preinit/`)
123+
124+
A web server demonstrating advanced observability initialization patterns, including pre-initializing the observability plugin before the LaunchDarkly client.
125+
126+
**Features:**
127+
- Uses `ldobserve.PreInitialize()` to initialize observability before the LaunchDarkly client
128+
- Demonstrates using observability without the LaunchDarkly client (with limited features)
129+
- Shows the `NewObservabilityPluginWithoutInit()` pattern for pre-initialized observability
130+
- Uses standard `net/http` package with OpenTelemetry instrumentation
131+
- Includes graceful shutdown handling
132+
- Demonstrates manual span creation and attribute setting
133+
134+
**To run:**
135+
```bash
136+
go run cmd/preinit/preinit.go
137+
```
138+
139+
**Endpoints:**
140+
- `GET /rolldice` - Rolls a dice and returns the result, with verbose output controlled by the `verbose-response` feature flag
141+
142+
**Use Cases:**
143+
- Advanced initialization scenarios where you need observability before the LaunchDarkly client
144+
- Using observability features without a LaunchDarkly client (some features will be unavailable)
145+
- Custom initialization timing requirements
120146

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
"net"
7+
"net/http"
8+
"os"
9+
"os/signal"
10+
"time"
11+
12+
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
13+
"go.opentelemetry.io/otel/attribute"
14+
"go.opentelemetry.io/otel/trace"
15+
16+
ld "github.com/launchdarkly/go-server-sdk/v7"
17+
"github.com/launchdarkly/go-server-sdk/v7/ldplugins"
18+
ldobserve "github.com/launchdarkly/observability-sdk/go"
19+
20+
appcontext "dice/internal/context"
21+
"dice/internal/dice"
22+
"dice/internal/version"
23+
)
24+
25+
func main() {
26+
if err := run(); err != nil {
27+
log.Fatalln(err)
28+
}
29+
}
30+
31+
func run() (err error) {
32+
// Handle SIGINT (CTRL+C) gracefully.
33+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
34+
defer stop()
35+
36+
// Initialize the observability plugin ahead of the LaunchDarkly client.
37+
// This is generally only required for advanced use cases and is not the
38+
// recommended way to initialize observability.
39+
// Observability can also be used without the LaunchDarkly client at all
40+
// by using this method. Some features will not be available when using
41+
// observability without the LaunchDarkly client.
42+
ldobserve.PreInitialize(os.Getenv("LAUNCHDARKLY_SDK_KEY"),
43+
ldobserve.WithEnvironment("test"),
44+
ldobserve.WithServiceName("go-plugin-example"),
45+
ldobserve.WithServiceVersion(version.Commit),
46+
)
47+
48+
client, _ := ld.MakeCustomClient(os.Getenv("LAUNCHDARKLY_SDK_KEY"),
49+
ld.Config{
50+
Plugins: []ldplugins.Plugin{
51+
// This special form of constructing the observability plugin
52+
// is used when observability has been pre-initialized.
53+
ldobserve.NewObservabilityPluginWithoutInit(),
54+
},
55+
}, 5*time.Second)
56+
57+
ctx = appcontext.WithLaunchDarklyClient(ctx, client)
58+
59+
// Start HTTP server.
60+
srv := &http.Server{
61+
Addr: ":8080",
62+
BaseContext: func(_ net.Listener) context.Context { return ctx },
63+
ReadTimeout: time.Second,
64+
WriteTimeout: 10 * time.Second,
65+
Handler: newHTTPHandler(),
66+
}
67+
srvErr := make(chan error, 1)
68+
go func() {
69+
srvErr <- srv.ListenAndServe()
70+
}()
71+
72+
// Wait for interruption.
73+
select {
74+
case err = <-srvErr:
75+
// Error when starting HTTP server.
76+
return err
77+
case <-ctx.Done():
78+
stop()
79+
}
80+
81+
ldobserve.Shutdown()
82+
83+
// When Shutdown is called, ListenAndServe immediately returns ErrServerClosed.
84+
err = srv.Shutdown(context.Background())
85+
return err
86+
}
87+
88+
func newHTTPHandler() http.Handler {
89+
mux := http.NewServeMux()
90+
91+
_, span := ldobserve.StartSpan(context.Background(), "test-span", []trace.SpanStartOption{})
92+
span.SetAttributes(attribute.String("test-attribute", "test-value"))
93+
span.End()
94+
95+
// handleFunc is a replacement for mux.HandleFunc
96+
// which enriches the handler's HTTP instrumentation with the pattern as the http.route.
97+
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
98+
// Configure the "http.route" for the HTTP instrumentation.
99+
handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
100+
mux.Handle(pattern, handler)
101+
}
102+
103+
// Register handlers.Als
104+
handleFunc("/rolldice", dice.Rolldice)
105+
106+
// Add HTTP instrumentation for the whole server.
107+
handler := otelhttp.NewHandler(
108+
mux,
109+
"/",
110+
otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
111+
// Return the route as the span name
112+
return r.URL.Path
113+
}))
114+
return handler
115+
}

go/initialize.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package ldobserve
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/Khan/genqlient/graphql"
8+
"go.opentelemetry.io/otel/attribute"
9+
"go.opentelemetry.io/otel/sdk/trace"
10+
semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
11+
12+
"github.com/launchdarkly/observability-sdk/go/attributes"
13+
"github.com/launchdarkly/observability-sdk/go/internal/gql"
14+
"github.com/launchdarkly/observability-sdk/go/internal/logging"
15+
"github.com/launchdarkly/observability-sdk/go/internal/metadata"
16+
"github.com/launchdarkly/observability-sdk/go/internal/otel"
17+
)
18+
19+
func getSamplingConfig(projectId string, config observabilityConfig) (*gql.GetSamplingConfigResponse, error) {
20+
var ctx context.Context
21+
if config.context != nil {
22+
ctx = config.context
23+
} else {
24+
ctx = context.Background()
25+
}
26+
client := graphql.NewClient(config.backendURL, http.DefaultClient)
27+
return gql.GetSamplingConfig(ctx, client, projectId)
28+
}
29+
30+
func setupOtel(sdkKey string, config observabilityConfig) {
31+
attributes := []attribute.KeyValue{
32+
semconv.TelemetryDistroNameKey.String(metadata.InstrumentationName),
33+
semconv.TelemetryDistroVersionKey.String(metadata.InstrumentationVersion),
34+
attribute.String(attributes.ProjectIDAttribute, sdkKey),
35+
}
36+
if config.environment != "" {
37+
attributes = append(attributes, semconv.DeploymentEnvironmentName(config.environment))
38+
}
39+
if config.serviceName != "" {
40+
attributes = append(attributes, semconv.ServiceNameKey.String(config.serviceName))
41+
}
42+
if config.serviceVersion != "" {
43+
attributes = append(attributes, semconv.ServiceVersionKey.String(config.serviceVersion))
44+
}
45+
if config.debug {
46+
logging.SetLogger(logging.ConsoleLogger{})
47+
}
48+
49+
var s trace.Sampler
50+
if len(config.samplingRateMap) > 0 {
51+
s = getSampler(config.samplingRateMap)
52+
} else {
53+
s = nil
54+
}
55+
otel.SetConfig(otel.Config{
56+
OtlpEndpoint: config.otlpEndpoint,
57+
ResourceAttributes: attributes,
58+
Sampler: s,
59+
})
60+
if !config.manualStart {
61+
err := otel.StartOTLP()
62+
if err != nil {
63+
logging.GetLogger().Errorf("failed to start otel: %v", err)
64+
}
65+
}
66+
go func() {
67+
cfg, err := getSamplingConfig(sdkKey, config)
68+
if err != nil {
69+
logging.GetLogger().Errorf("failed to get sampling config: %v", err)
70+
return
71+
}
72+
logging.GetLogger().Infof("got sampling config: %v", cfg)
73+
otel.SetSamplingConfig(cfg)
74+
}()
75+
if config.context != nil {
76+
go func() {
77+
<-config.context.Done()
78+
otel.Shutdown()
79+
}()
80+
}
81+
}
82+
83+
// PreInitialize initializes the observability plugin independently of the
84+
// LaunchDarkly client.
85+
//
86+
// In most situations the plugin should be used instead of this function.
87+
// In cases where the usage of observability needs to be flagged, the plugin
88+
// can be used with the WithManualStart option.
89+
//
90+
// This function is provided for situations where the LaunchDarkly client is not
91+
// readily available, or when observability needs to be initialized earlier than
92+
// the LaunchDarkly client.
93+
func PreInitialize(sdkKey string, opts ...Option) {
94+
config := defaultConfig()
95+
for _, opt := range opts {
96+
opt(&config)
97+
}
98+
99+
setupOtel(sdkKey, config)
100+
}

go/plugin.go

Lines changed: 13 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,15 @@
55
package ldobserve
66

77
import (
8-
"context"
9-
"net/http"
10-
11-
"github.com/Khan/genqlient/graphql"
12-
13-
"github.com/launchdarkly/observability-sdk/go/attributes"
14-
"github.com/launchdarkly/observability-sdk/go/internal/gql"
15-
"github.com/launchdarkly/observability-sdk/go/internal/logging"
16-
"github.com/launchdarkly/observability-sdk/go/internal/metadata"
17-
18-
"go.opentelemetry.io/otel/attribute"
19-
"go.opentelemetry.io/otel/sdk/trace"
20-
semconv "go.opentelemetry.io/otel/semconv/v1.34.0"
21-
228
"github.com/launchdarkly/go-server-sdk/ldotel"
239
"github.com/launchdarkly/go-server-sdk/v7/interfaces"
2410
"github.com/launchdarkly/go-server-sdk/v7/ldhooks"
2511
"github.com/launchdarkly/go-server-sdk/v7/ldplugins"
26-
"github.com/launchdarkly/observability-sdk/go/internal/otel"
2712
)
2813

2914
// ObservabilityPlugin represents the LaunchDarkly observability plugin.
3015
type ObservabilityPlugin struct {
31-
config observabilityConfig
16+
config *observabilityConfig
3217
ldplugins.Unimplemented
3318
}
3419

@@ -40,10 +25,18 @@ func NewObservabilityPlugin(opts ...Option) *ObservabilityPlugin {
4025
}
4126

4227
return &ObservabilityPlugin{
43-
config: config,
28+
config: &config,
4429
}
4530
}
4631

32+
// NewObservabilityPluginWithoutInit creates a new observability plugin without
33+
// for performing initialization.
34+
// This method generally does not need to be used, and should only be used in
35+
// conjunction with InitializeWithoutPlugin.
36+
func NewObservabilityPluginWithoutInit() *ObservabilityPlugin {
37+
return &ObservabilityPlugin{}
38+
}
39+
4740
// GetHooks returns the hooks for the observability plugin.
4841
func (p ObservabilityPlugin) GetHooks(_ ldplugins.EnvironmentMetadata) []ldhooks.Hook {
4942
return []ldhooks.Hook{
@@ -56,67 +49,10 @@ func (p ObservabilityPlugin) Metadata() ldplugins.Metadata {
5649
return ldplugins.NewMetadata("launchdarkly-observability")
5750
}
5851

59-
func (p ObservabilityPlugin) getSamplingConfig(projectId string) (*gql.GetSamplingConfigResponse, error) {
60-
var ctx context.Context
61-
if p.config.context != nil {
62-
ctx = p.config.context
63-
} else {
64-
ctx = context.Background()
65-
}
66-
client := graphql.NewClient(p.config.backendURL, http.DefaultClient)
67-
return gql.GetSamplingConfig(ctx, client, projectId)
68-
}
69-
7052
// Register registers the observability plugin with the LaunchDarkly client.
7153
func (p ObservabilityPlugin) Register(client interfaces.LDClientInterface, ldmd ldplugins.EnvironmentMetadata) {
72-
attributes := []attribute.KeyValue{
73-
semconv.TelemetryDistroNameKey.String(metadata.InstrumentationName),
74-
semconv.TelemetryDistroVersionKey.String(metadata.InstrumentationVersion),
75-
attribute.String(attributes.ProjectIDAttribute, ldmd.SdkKey),
76-
}
77-
if p.config.environment != "" {
78-
attributes = append(attributes, semconv.DeploymentEnvironmentName(p.config.environment))
79-
}
80-
if p.config.serviceName != "" {
81-
attributes = append(attributes, semconv.ServiceNameKey.String(p.config.serviceName))
82-
}
83-
if p.config.serviceVersion != "" {
84-
attributes = append(attributes, semconv.ServiceVersionKey.String(p.config.serviceVersion))
85-
}
86-
if p.config.debug {
87-
logging.SetLogger(logging.ConsoleLogger{})
88-
}
89-
90-
var s trace.Sampler
91-
if len(p.config.samplingRateMap) > 0 {
92-
s = getSampler(p.config.samplingRateMap)
93-
} else {
94-
s = nil
95-
}
96-
otel.SetConfig(otel.Config{
97-
OtlpEndpoint: p.config.otlpEndpoint,
98-
ResourceAttributes: attributes,
99-
Sampler: s,
100-
})
101-
if !p.config.manualStart {
102-
err := otel.StartOTLP()
103-
if err != nil {
104-
logging.GetLogger().Errorf("failed to start otel: %v", err)
105-
}
106-
}
107-
go func() {
108-
cfg, err := p.getSamplingConfig(ldmd.SdkKey)
109-
if err != nil {
110-
logging.GetLogger().Errorf("failed to get sampling config: %v", err)
111-
return
112-
}
113-
logging.GetLogger().Infof("got sampling config: %v", cfg)
114-
otel.SetSamplingConfig(cfg)
115-
}()
116-
if p.config.context != nil {
117-
go func() {
118-
<-p.config.context.Done()
119-
otel.Shutdown()
120-
}()
54+
if p.config == nil {
55+
return
12156
}
57+
setupOtel(ldmd.SdkKey, *p.config)
12258
}

0 commit comments

Comments
 (0)