Skip to content

feat: add initial implementation for Java Observability Plugin#381

Open
Vadman97 wants to merge 2 commits intomainfrom
vadim/java-plugin
Open

feat: add initial implementation for Java Observability Plugin#381
Vadman97 wants to merge 2 commits intomainfrom
vadim/java-plugin

Conversation

@Vadman97
Copy link
Contributor

@Vadman97 Vadman97 commented Feb 19, 2026

Summary

Add new Java O11y plugin.

How did you test this change?

ci

Are there any deployment considerations?


Note

Medium Risk
Introduces a new Java SDK that performs background network calls and installs global OpenTelemetry providers/exporters, which can impact telemetry behavior and runtime resources even though it’s largely additive and isolated.

Overview
Adds a new sdk/@launchdarkly/observability-java Gradle module shipping a LaunchDarkly Java Server SDK plugin (ObservabilityPlugin) that configures OpenTelemetry tracing/logs/metrics export to LaunchDarkly and exposes a static LDObserve API for manual instrumentation.

The plugin fetches backend-driven sampling rules via a GraphQL call and applies them at export time (trace exporter + log processor) to control telemetry volume, and includes initial unit tests plus repository automation (new Java Observability GitHub Actions workflow and release-please package config for the Java artifact).

Written by Cursor Bugbot for commit d73b95c. This will update automatically on new commits. Configure here.

@Vadman97 Vadman97 requested a review from a team as a code owner February 19, 2026 20:22
@Vadman97 Vadman97 changed the title Vadim/java plugin feat: add initial implementation for Java Observability Plugin Feb 19, 2026
@Vadman97 Vadman97 changed the title feat: add initial implementation for Java Observability Plugin feat: add initial implementation for Java Observability Plugin Feb 19, 2026
.setName(name)
.setKind(SpanKind.INTERNAL)
.setSpanContext(SpanContext.create(
"00000000000000000000000000000001",

Check failure

Code scanning / devskim

A token or key was found in source code. If this represents a secret, it should be moved somewhere else. Error test

Do not store tokens or keys in source code.
.serviceName("test-service")
.serviceVersion("2.0.0")
.environment("staging")
.otlpEndpoint("http://localhost:4318")

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note test

Do not leave debug code in production
.serviceVersion("2.0.0")
.environment("staging")
.otlpEndpoint("http://localhost:4318")
.backendUrl("http://localhost:8080")

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note test

Do not leave debug code in production
assertEquals("test-service", options.getServiceName());
assertEquals("2.0.0", options.getServiceVersion());
assertEquals("staging", options.getEnvironment());
assertEquals("http://localhost:4318", options.getOtlpEndpoint());

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note test

Do not leave debug code in production
assertEquals("2.0.0", options.getServiceVersion());
assertEquals("staging", options.getEnvironment());
assertEquals("http://localhost:4318", options.getOtlpEndpoint());
assertEquals("http://localhost:8080", options.getBackendUrl());

Check notice

Code scanning / devskim

Accessing localhost could indicate debug code, or could hinder scaling. Note test

Do not leave debug code in production
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 5 potential issues.

@@ -0,0 +1,2 @@
#Thu Feb 19 11:31:22 CST 2026
gradle.version=8.14.3
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gradle build cache accidentally committed to repository

Low Severity

The .gradle/buildOutputCleanup/cache.properties file is a Gradle-generated build cache artifact that shouldn't be checked in. The sibling Android observability project at sdk/@launchdarkly/observability-android/ has a .gitignore that excludes .gradle, but the new Java project has no .gitignore at all, allowing this build artifact to slip through.

Fix in Cursor Fix in Web


OtelManager manager = new OtelManager(
tracerProvider, loggerProvider, meterProvider, sdk, customSampler);
INSTANCE.set(manager);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition in OtelManager singleton initialization

Medium Severity

initialize() uses a check-then-act pattern — INSTANCE.get() != null followed by INSTANCE.set(manager) — which is not atomic. Two concurrent callers can both pass the null check, both construct full OTel provider stacks, and both call set(). The first manager's providers are leaked (never shut down), a duplicate JVM shutdown hook is registered, and buildAndRegisterGlobal() throws for the second caller. Using compareAndSet(null, manager) and cleaning up on failure would prevent this.

Fix in Cursor Fix in Web

if (result.isSampled()) {
sampled.add(span);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trace exporter drops sampling ratio attributes from spans

Medium Severity

SamplingTraceExporter.export() discards result.getAttributes() (containing the highlight.sampling.ratio) when adding sampled spans to the export list. The sibling SamplingLogProcessor correctly merges these attributes into log records, and the Android SamplingTraceExporter does the same for spans via cloneSpanDataWithAttributes. This means the backend won't see sampling ratio metadata on exported spans, breaking observability accounting.

Fix in Cursor Fix in Web

private final String pattern;
public RegexMatch(String pattern) { this.pattern = pattern; }
public String getPattern() { return pattern; }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused ValueMatch and RegexMatch classes are dead code

Low Severity

ValueMatch and RegexMatch inner classes are defined but never instantiated or referenced anywhere in the codebase. All matching logic uses MatchConfig instead (which encapsulates both value and regex matching). These appear to be leftover types that were superseded by the unified MatchConfig class.

Fix in Cursor Fix in Web

}
}, "ld-observability-sampling");
samplingThread.setDaemon(true);
samplingThread.start();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual start mode silently loses sampling configuration

Medium Severity

When manualStart is true, register() skips OtelManager.initialize() but still launches the background thread that fetches sampling config. setSamplingConfig() silently discards the config because INSTANCE is null. When the user later calls LDObserve.start(), a fresh CustomSampler is created with no config, and no retry ever happens. This permanently disables sampling in manual start mode.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant