feat: add initial implementation for Java Observability Plugin#381
feat: add initial implementation for Java Observability Plugin#381
Conversation
| .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
| .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
| .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
| 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
| 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
| @@ -0,0 +1,2 @@ | |||
| #Thu Feb 19 11:31:22 CST 2026 | |||
| gradle.version=8.14.3 | |||
There was a problem hiding this comment.
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.
|
|
||
| OtelManager manager = new OtelManager( | ||
| tracerProvider, loggerProvider, meterProvider, sdk, customSampler); | ||
| INSTANCE.set(manager); |
There was a problem hiding this comment.
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.
| if (result.isSampled()) { | ||
| sampled.add(span); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| private final String pattern; | ||
| public RegexMatch(String pattern) { this.pattern = pattern; } | ||
| public String getPattern() { return pattern; } | ||
| } |
There was a problem hiding this comment.
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.
| } | ||
| }, "ld-observability-sampling"); | ||
| samplingThread.setDaemon(true); | ||
| samplingThread.start(); |
There was a problem hiding this comment.
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.


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-javaGradle module shipping a LaunchDarkly Java Server SDK plugin (ObservabilityPlugin) that configures OpenTelemetry tracing/logs/metrics export to LaunchDarkly and exposes a staticLDObserveAPI 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 ObservabilityGitHub Actions workflow andrelease-pleasepackage config for the Java artifact).Written by Cursor Bugbot for commit d73b95c. This will update automatically on new commits. Configure here.