Skip to content

feat(java): add setAttributes and custom span/log processors - #861

Open
abelonogov-ld wants to merge 2 commits into
mainfrom
andrey/global-attributes-java
Open

abelonogov-ld wants to merge 2 commits into
mainfrom
andrey/global-attributes-java

Conversation

@abelonogov-ld

@abelonogov-ld abelonogov-ld commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds global attributes and custom span/log processors to the Java server observability plugin. This is one of a set of matching PRs across RN, Android, iOS, Flutter, MAUI, Java and Python that share the same semantics (see #858, #859, #860, launchdarkly/swift-launchdarkly-observability#290).

Global attributes

  • LDObserve.setAttributes(Attributes) / LDObserve.removeAttributes(Collection<String>) keep one process-wide attribute set (AtomicReference to an immutable snapshot). Safe before init and thread-safe.
  • Applied to every span the plugin's tracer provider starts (manual spans, flag evaluations, error spans, and instrumentation wired to LDObserve.getOpenTelemetry()), every log record, and metrics recorded through LDObserve.
  • Precedence: per-call / span-builder values > globals > resource. Globals only fill missing keys (matched by name, regardless of type); spans get the values current at start.
  • Globals survive LDObserve.shutdown() (process-wide lifetime).

Custom processors (init-time options)

  • ObservabilityOptions.Builder.spanProcessors(List<SpanProcessor>) / logProcessors(List<LogRecordProcessor>), plus spanProcessor(...) / logProcessor(...) to add one at a time (mirrors customHeaders / customHeader).
  • Order: global attributes → user processors (in order) → the plugin's level filter, sampling and batch export, so user changes are exported and sampling still applies.
  • Each user call is wrapped in try/catch: a throwing processor is logged (WARNING once, then FINE) and export continues; forceFlush / shutdown are forwarded and return a failed result instead of propagating.

Dependency change: opentelemetry-sdk moves from implementation to api (compile scope in the published POM), because ObservabilityOptions now exposes SDK processor types. checkPublishedPom enforces this. Same version; it was already a transitive runtime dependency.

Not covered: metrics recorded on a meter obtained via getOpenTelemetry() (only LDObserve metric methods merge globals); documented in the README.

How did you test this change?

  • ./gradlew :lib:build (javadoc, POM, classpath and fixture checks, tests) and ./gradlew :lib:test -PldServerSdkVersion=7.10.1 (as CI) pass: 146 tests, 0 failures.
  • Unit tests (GlobalAttributesTest, SafeProcessorTest): merge/remove, name matching across types, per-call precedence, null/empty input, concurrent writes, span and log filling, throwing processors, flush/shutdown forwarding.
  • End-to-end PluginUserFlowTest with a real LDClient and a local OTLP collector: globals set before start reach flag-evaluation, manual and error spans, logs and metrics; per-call values win; removed keys are not applied; spans keep start-time values; user processors run in order and their changes reach the exported payload; sampling still drops matching spans; throwing processors don't break export, flush or shutdown.
  • A mutation check (removing the wrapping and the global-attributes processor) makes the new end-to-end tests fail.
  • javap confirms every SDK method used exists in OpenTelemetry 1.38.0, the plugin's minimum.

Are there any deployment considerations?

  • The published POM lists opentelemetry-sdk at compile scope instead of runtime scope; consumers see SDK types on their compile classpath.
  • Purely additive API; no behavior change unless these APIs are used.
  • Processors are read when the providers start; a second LDClient with a different SDK key doesn't add its processors, consistent with how its other options are already ignored.

Note

Overview
Adds process-wide global attributes and user-defined OpenTelemetry processors to the Java observability plugin, aligned with other LaunchDarkly SDKs.

Global attributes: New LDObserve.setAttributes / removeAttributes keep a thread-safe, merge-by-key store (safe before plugin init, survives shutdown). Globals are applied to exported spans and logs via dedicated processors in OtelManager, and to metrics recorded through LDObserve via GlobalAttributes.merge. Per-call or span-builder attributes win on key conflicts; spans snapshot globals at start.

Custom processors: ObservabilityOptions now accepts ordered SpanProcessor and LogRecordProcessor lists (single-add helpers too). They run after global-attribute injection and before level filtering, sampling, and export; failures are wrapped in SafeSpanProcessor / SafeLogRecordProcessor so export continues.

Packaging: opentelemetry-sdk is promoted to compile-scoped api because processor types are part of the public options API; POM checks were updated.

The Spring Boot e2e app sets UTM globals before startup and prints [#1] expectations on each request so manual verification in LaunchDarkly is straightforward.

Reviewed by Cursor Bugbot for commit be24b27. Bugbot is set up for automated code reviews on this repo. Configure here.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

This branch has not been deployed

No deployments
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.

2 participants