Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9a65f1c
feat: add initial implementation for Java Observability Plugin
Vadman97 Feb 19, 2026
afa87fb
ci
Vadman97 Feb 19, 2026
a3cd539
fix: make the Java observability plugin safe on a server
cursoragent Oct 2, 2026
ff7a8f2
fix: use the HTTPS Apache license URL in the Java POM
cursoragent Oct 2, 2026
81e30e0
fix: bound sampling regexes and stop poisoning the OTel global
cursoragent Oct 2, 2026
d45b31c
fix: reject nested quantifiers wrapped in another group
cursoragent Oct 2, 2026
1a3eed3
test: add a runnable Java observability plugin example
cursoragent Oct 2, 2026
4342f83
fix: publish the Java plugin as launchdarkly-java-server-sdk-observab…
Vadman97 Oct 19, 2026
ef0f4e3
feat: add enabled, resourceAttributes, customHeaders, proxy and logsA…
Vadman97 Oct 19, 2026
6d8682e
ci: test the Java plugin on JDK 11/17/21, as a Maven consumer, and as…
Vadman97 Oct 19, 2026
1bb34c3
test: add a Spring Boot web backend as the Java plugin e2e app
Vadman97 Oct 19, 2026
6ca7fc5
ci: name the dockerized Java e2e service after its directory
Vadman97 Oct 19, 2026
331597a
ci: format the e2e workflow matrix the way prettier wants it
Vadman97 Oct 19, 2026
8b258de
fix: tolerate JSON nulls in the sampling payload and export log sever…
Vadman97 Oct 19, 2026
c09b2a7
ci: suppress DevSkim loopback findings on the smoke test and e2e Dock…
Vadman97 Oct 19, 2026
9898a2c
ci: explain the dismissed DevSkim finding on the e2e Dockerfile
Vadman97 Oct 19, 2026
2f3acf0
ci: publish the Java plugin to Maven Central on release
Vadman97 Oct 19, 2026
d097278
fix: compile sampling regexes as sent and match substrings
Vadman97 Oct 20, 2026
b5c6e22
fix: record flag evaluations on the plugin's tracer and cache instrum…
Vadman97 Oct 20, 2026
55bead0
fix: always record spans on the plugin's tracer provider
Vadman97 Oct 20, 2026
694fda7
feat: let the Spring Boot example target another LaunchDarkly instance
Vadman97 Oct 20, 2026
8831f28
test: run the shared sampling scenarios and match log severityText
Vadman97 Oct 20, 2026
c440e6b
test: boot the Spring Boot example against a local collector
Vadman97 Oct 20, 2026
c4ad5f0
test: build scenario span ids with TraceId/SpanId helpers
Vadman97 Oct 20, 2026
3225375
fix: never throw from LDObserve on null arguments
Vadman97 Oct 22, 2026
5f24da7
fix: keep one set of providers when several LDClients register the pl…
Vadman97 Oct 22, 2026
9d2e9d0
build: keep LaunchDarkly artifacts out of the POM and state the SDK f…
Vadman97 Oct 22, 2026
ca8e984
fix: match sampling values by type, as the Go and .NET SDKs do
Vadman97 Oct 22, 2026
2a43c1d
fix: keep telemetry when sampling fails instead of throwing
Vadman97 Oct 22, 2026
b420dc4
test: cover the Java plugin in the pytest e2e suite
Vadman97 Oct 22, 2026
abb3e08
build: parse sampling JSON with LDValue and drop the Gson dependency
Vadman97 Oct 22, 2026
ad8d610
test: log the span name, not the whole span, in the .NET traces check
Vadman97 Oct 22, 2026
274fc0a
fix: keep the application running when the plugin cannot start
Vadman97 Oct 22, 2026
04bd6bb
fix: let frameworks own shutdown so drained requests keep their telem…
Vadman97 Oct 22, 2026
2743631
feat: expose the plugin's OpenTelemetry through LDObserve.getOpenTele…
Vadman97 Oct 22, 2026
2698137
fix: keep the OTLP exporters' own counters out of customer metrics
Vadman97 Oct 22, 2026
0b0fc99
fix: log an unreachable sampling backend in one line
Vadman97 Oct 22, 2026
253ce92
fix: make LDObserve.recordMetric a no-op on OpenTelemetry before 1.38
Vadman97 Oct 22, 2026
0a58be1
fix: bound shutdown to 5 s when the endpoint is unreachable
Vadman97 Oct 22, 2026
2cac521
feat: log where telemetry goes at startup and warn without a service …
Vadman97 Oct 22, 2026
79afcf9
docs: cover Spring Boot, OpenTelemetry versions and the Java agent in…
Vadman97 Oct 22, 2026
bd1831a
Merge remote-tracking branch 'origin/main' into work
Vadman97 Oct 22, 2026
047c5d3
ci: test the Java plugin against the oldest supported LaunchDarkly SD…
Vadman97 Oct 22, 2026
2425699
fix: drop the duplicate exception.stacktrace span attribute in record…
Vadman97 Oct 26, 2026
e75b5a7
fix: fetch the sampling config when LDObserve.start is used without t…
Vadman97 Oct 26, 2026
ac160d1
ci: publish package hashes and SLSA provenance for the Java plugin
Vadman97 Oct 26, 2026
77e6fff
fix: record an error on the current span when the plugin started it
Vadman97 Oct 27, 2026
59f581f
build: compile with options.release instead of source/target compatib…
Vadman97 Oct 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Used by builds whose context is the repository root (e2e/java-plugin/Dockerfile).
.git
**/node_modules
**/build
**/.gradle
rrweb
88 changes: 88 additions & 0 deletions .github/actions/publish-java-sdk/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: 'Publish Java SDK'
description: 'Build and publish the Java server observability plugin to Maven Central'

inputs:
workspace-path:
description: 'Path to the Java SDK workspace'
required: true
default: 'sdk/@launchdarkly/observability-java'
java_version:
description: 'The Java version to use.'
required: false
default: '17'
java_distribution:
description: 'The Java distribution to use.'
required: false
default: 'temurin'
aws-role-arn:
description: 'AWS role ARN for accessing secrets'
required: true
dry-run:
description: 'Whether to run the publish in dry-run mode'
required: false
default: 'false'
prerelease:
description: 'Whether to publish a prerelease version'
required: false
default: 'false'

outputs:
package-hashes:
description: 'Base64 encoded SHA256 hashes of published packages'
value: ${{ steps.package-hashes.outputs.package-hashes }}

runs:
using: 'composite'
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: ${{ inputs.java_distribution }}
java-version: ${{ inputs.java_version }}

# Same Sonatype Central account and code-signing key as the Android
# plugin; both publish under the com.launchdarkly namespace.
- uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
name: Get secrets
with:
aws_assume_role: ${{ inputs.aws-role-arn }}
ssm_parameter_pairs: |
/production/common/releasing/sonatype/central/username = SONATYPE_USER_NAME,
/production/common/releasing/sonatype/central/password = SONATYPE_PASSWORD,
/production/common/releasing/android_code_signing/private_key_id = SIGNING_KEY_ID,
/production/common/releasing/android_code_signing/private_key_passphrase = SIGNING_KEY_PASSPHRASE
s3_path_pairs: 'launchdarkly-releaser/android/code-signing-keyring.gpg = code-signing-keyring.gpg'

- name: Publish Library
shell: bash
if: ${{ inputs.dry-run != 'true' }}
working-directory: ${{ inputs.workspace-path }}
env:
LD_RELEASE_IS_PRERELEASE: ${{ inputs.prerelease }}
SIGNING_KEY_ID: ${{ env.SIGNING_KEY_ID }}
SIGNING_KEY_PASSPHRASE: ${{ env.SIGNING_KEY_PASSPHRASE }}
SIGNING_SECRET_KEY_RING_FILE: ${{ github.workspace }}/code-signing-keyring.gpg
SONATYPE_USER_NAME: ${{ env.SONATYPE_USER_NAME }}
SONATYPE_PASSWORD: ${{ env.SONATYPE_PASSWORD }}
run: source $GITHUB_ACTION_PATH/publish.sh

# A dry run still builds and verifies the artifact (unit tests, the
# runtime-classpath guard and the POM guard) so a broken release is
# caught before anyone flips dry-run off.
- name: Dry Run Publish Library
shell: bash
if: ${{ inputs.dry-run == 'true' }}
working-directory: ${{ inputs.workspace-path }}
run: |
./gradlew :lib:build --no-daemon --console=plain
echo "Dry run. Not publishing."

- name: Hash build files for provenance
id: package-hashes
shell: bash
working-directory: ${{ inputs.workspace-path }}/lib/build/libs
run: |
echo "package-hashes=$(sha256sum launchdarkly-java-server-sdk-observability-*.jar | base64 -w0)" >> "$GITHUB_OUTPUT"
37 changes: 37 additions & 0 deletions .github/actions/publish-java-sdk/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

set -ue

GRADLE_ARGS=(
--no-daemon
--console=plain
-Psigning.keyId="${SIGNING_KEY_ID}"
-Psigning.password="${SIGNING_KEY_PASSPHRASE}"
-Psigning.secretKeyRingFile="${SIGNING_SECRET_KEY_RING_FILE}"
-PsonatypeUsername="${SONATYPE_USER_NAME}"
-PsonatypePassword="${SONATYPE_PASSWORD}"
)

# `check` runs the unit tests plus the guards on what gets published: no
# OkHttp/Okio/Kotlin on the runtime classpath, and opentelemetry-api at
# compile scope in the POM. Nothing is uploaded if any of them fail.
echo "Verifying the artifact"
./gradlew :lib:build "${GRADLE_ARGS[@]}" || {
echo "Gradle build/verification failed" >&2
exit 1
}

echo "Publishing to Sonatype"
if [ "${LD_RELEASE_IS_PRERELEASE}" == "true" ]; then
echo "PRERELEASE"
./gradlew publishToSonatype "${GRADLE_ARGS[@]}" || {
echo "Gradle publish/release failed" >&2
exit 1
}
else
echo "RELEASE"
./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository "${GRADLE_ARGS[@]}" || {
echo "Gradle publish/release failed" >&2
exit 1
}
fi
10 changes: 9 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ jobs:
runs-on: ubuntu-22.04-8core-32gb
strategy:
matrix:
app: ['dotnet', 'dotnet4', 'go', 'python', 'ruby']
app:
[
'dotnet',
'dotnet4',
'go',
'java-plugin-spring-boot',
'python',
'ruby',
]

steps:
- uses: actions/checkout@v4
Expand Down
104 changes: 104 additions & 0 deletions .github/workflows/java-observability.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: 'Java Observability'

on:
push:
branches: ['main']
paths:
- 'sdk/@launchdarkly/observability-java/**'
- 'e2e/java-plugin/**'
- 'e2e/java-plugin-spring-boot/**'
# Shared sampling scenarios; :lib:checkSamplingFixtures compares them.
- 'go/internal/otel/*-test-scenarios.json'
- '.github/workflows/java-observability.yml'
pull_request:
types: [opened, synchronize]
paths:
- 'sdk/@launchdarkly/observability-java/**'
- 'e2e/java-plugin/**'
- 'e2e/java-plugin-spring-boot/**'
# Shared sampling scenarios; :lib:checkSamplingFixtures compares them.
- 'go/internal/otel/*-test-scenarios.json'
- '.github/workflows/java-observability.yml'
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

permissions:
contents: read

concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
test:
# The library targets Java 11, so it is built and tested on every LTS
# a server might run it on, not only the newest.
name: Unit Tests (JDK ${{ matrix.java }})
runs-on: ubuntu-22.04-8core-32gb
strategy:
fail-fast: false
matrix:
java: [11, 17, 21]
defaults:
run:
working-directory: ./sdk/@launchdarkly/observability-java
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
cache: 'gradle'

# `build` includes `check`, which runs the unit tests plus the
# runtime-classpath guard (no OkHttp/Okio/Kotlin) and the POM
# guard (opentelemetry-api at compile scope, JDK sender at runtime).
- name: Build, test and verify the published POM
run: ./gradlew :lib:build --no-daemon --console=plain

# The plugin needs the Plugin API, first shipped in SDK 7.10.1.
- name: Test against the oldest supported LaunchDarkly SDK
if: matrix.java == 11
run: ./gradlew :lib:test -PldServerSdkVersion=7.10.1 --no-daemon --console=plain

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: java-unit-test-results-jdk${{ matrix.java }}
path: sdk/@launchdarkly/observability-java/**/build/test-results/**/TEST-*.xml
retention-days: 7

consumer:
# Builds the e2e example against the artifact as a consumer sees it:
# published to Maven local and resolved through its POM, with no
# composite build to paper over scope or transitive-dependency mistakes.
name: Consumer check (JDK ${{ matrix.java }})
runs-on: ubuntu-22.04-8core-32gb
strategy:
fail-fast: false
matrix:
java: [11, 21]
defaults:
run:
working-directory: ./e2e/java-plugin
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: ${{ matrix.java }}
cache: 'gradle'

- name: Publish the plugin to Maven local and build the example against it
run: make consumer-check

# Spring Boot 3 needs Java 17, so the web backend is checked as a
# consumer on the newer JDK only.
- name: Build the Spring Boot backend against the Maven local artifact
if: matrix.java != 11
working-directory: ./e2e/java-plugin-spring-boot
run: make consumer-check
90 changes: 90 additions & 0 deletions .github/workflows/java-plugin-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Java Observability Plugin E2E

on:
push:
branches: ['main']
paths:
- 'e2e/java-plugin/**'
- 'e2e/java-plugin-spring-boot/**'
- 'sdk/@launchdarkly/observability-java/**'
- '.github/workflows/java-plugin-e2e.yml'
pull_request:
types: [opened, synchronize]
paths:
- 'e2e/java-plugin/**'
- 'e2e/java-plugin-spring-boot/**'
- 'sdk/@launchdarkly/observability-java/**'
- '.github/workflows/java-plugin-e2e.yml'

permissions:
contents: read

concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
run:
name: Run example
runs-on: ubuntu-22.04-8core-32gb
defaults:
run:
working-directory: ./e2e/java-plugin
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
cache: 'gradle'

# Runs once per owner of GlobalOpenTelemetry: the plugin, a no-op
# claimed before the client starts, and the Java agent.
- name: Run the self-checking example
run: make run

# Builds the Spring Boot web backend, starts it offline (no SDK
# key) and drives every route. It must come up, answer, record the
# /error exception as a 500, and stop cleanly on SIGTERM with the
# plugin flushing on the way out.
- name: Build and test the Spring Boot backend
working-directory: ./e2e/java-plugin-spring-boot
run: make test build

- name: Smoke-test the Spring Boot backend
working-directory: ./e2e/java-plugin-spring-boot
# The app under test runs on this runner, so it is reached on the
# loopback address. DevSkim DS162092 flags that as debug code.
env:
APP: http://localhost:8080 # DevSkim: ignore DS162092
run: |
java -jar build/libs/*.jar > app.log 2>&1 &
APP_PID=$!
for i in $(seq 1 120); do
if curl -sf -o /dev/null "$APP/"; then break; fi
if ! kill -0 "$APP_PID" 2>/dev/null; then echo "app exited early"; cat app.log; exit 1; fi
sleep 1
done
curl -sf "$APP/" || { cat app.log; exit 1; }
curl -sf -H 'X-User-Key: ci' "$APP/rolldice" || { cat app.log; exit 1; }
curl -sf "$APP/flags/example-greeting" || { cat app.log; exit 1; }
curl -sf "$APP/actuator/health" || { cat app.log; exit 1; }
code=$(curl -s -o /dev/null -w '%{http_code}' "$APP/error")
[ "$code" = "500" ] || { echo "expected 500 from /error, got $code"; cat app.log; exit 1; }
kill -TERM "$APP_PID"
wait "$APP_PID" || true
grep -q "Closing LaunchDarkly Client" app.log || { echo "client did not close on shutdown"; cat app.log; exit 1; }
echo "Spring Boot backend OK"

# The OpenTelemetry Java agent owns GlobalOpenTelemetry here, as
# it does in many production services. Flag evaluations and
# request spans must still reach the plugin's exporter, checked
# against a local collector.
- name: Smoke-test the Spring Boot backend under the Java agent
working-directory: ./e2e/java-plugin-spring-boot
run: make javaagent-smoke

- name: Build the dockerized Spring Boot backend
working-directory: .
run: docker build -f e2e/java-plugin-spring-boot/Dockerfile -t e2e-java .
Loading
Loading