Repository navigation
feat: add initial implementation for Java Observability Plugin #381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+8,809
−3
Merged
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 afa87fb
ci
Vadman97 a3cd539
fix: make the Java observability plugin safe on a server
cursoragent ff7a8f2
fix: use the HTTPS Apache license URL in the Java POM
cursoragent 81e30e0
fix: bound sampling regexes and stop poisoning the OTel global
cursoragent d45b31c
fix: reject nested quantifiers wrapped in another group
cursoragent 1a3eed3
test: add a runnable Java observability plugin example
cursoragent 4342f83
fix: publish the Java plugin as launchdarkly-java-server-sdk-observab…
Vadman97 ef0f4e3
feat: add enabled, resourceAttributes, customHeaders, proxy and logsA…
Vadman97 6d8682e
ci: test the Java plugin on JDK 11/17/21, as a Maven consumer, and as…
Vadman97 1bb34c3
test: add a Spring Boot web backend as the Java plugin e2e app
Vadman97 6ca7fc5
ci: name the dockerized Java e2e service after its directory
Vadman97 331597a
ci: format the e2e workflow matrix the way prettier wants it
Vadman97 8b258de
fix: tolerate JSON nulls in the sampling payload and export log sever…
Vadman97 c09b2a7
ci: suppress DevSkim loopback findings on the smoke test and e2e Dock…
Vadman97 9898a2c
ci: explain the dismissed DevSkim finding on the e2e Dockerfile
Vadman97 2f3acf0
ci: publish the Java plugin to Maven Central on release
Vadman97 d097278
fix: compile sampling regexes as sent and match substrings
Vadman97 b5c6e22
fix: record flag evaluations on the plugin's tracer and cache instrum…
Vadman97 55bead0
fix: always record spans on the plugin's tracer provider
Vadman97 694fda7
feat: let the Spring Boot example target another LaunchDarkly instance
Vadman97 8831f28
test: run the shared sampling scenarios and match log severityText
Vadman97 c440e6b
test: boot the Spring Boot example against a local collector
Vadman97 c4ad5f0
test: build scenario span ids with TraceId/SpanId helpers
Vadman97 3225375
fix: never throw from LDObserve on null arguments
Vadman97 5f24da7
fix: keep one set of providers when several LDClients register the pl…
Vadman97 9d2e9d0
build: keep LaunchDarkly artifacts out of the POM and state the SDK f…
Vadman97 ca8e984
fix: match sampling values by type, as the Go and .NET SDKs do
Vadman97 2a43c1d
fix: keep telemetry when sampling fails instead of throwing
Vadman97 b420dc4
test: cover the Java plugin in the pytest e2e suite
Vadman97 abb3e08
build: parse sampling JSON with LDValue and drop the Gson dependency
Vadman97 ad8d610
test: log the span name, not the whole span, in the .NET traces check
Vadman97 274fc0a
fix: keep the application running when the plugin cannot start
Vadman97 04bd6bb
fix: let frameworks own shutdown so drained requests keep their telem…
Vadman97 2743631
feat: expose the plugin's OpenTelemetry through LDObserve.getOpenTele…
Vadman97 2698137
fix: keep the OTLP exporters' own counters out of customer metrics
Vadman97 0b0fc99
fix: log an unreachable sampling backend in one line
Vadman97 253ce92
fix: make LDObserve.recordMetric a no-op on OpenTelemetry before 1.38
Vadman97 0a58be1
fix: bound shutdown to 5 s when the endpoint is unreachable
Vadman97 2cac521
feat: log where telemetry goes at startup and warn without a service …
Vadman97 79afcf9
docs: cover Spring Boot, OpenTelemetry versions and the Java agent in…
Vadman97 bd1831a
Merge remote-tracking branch 'origin/main' into work
Vadman97 047c5d3
ci: test the Java plugin against the oldest supported LaunchDarkly SD…
Vadman97 2425699
fix: drop the duplicate exception.stacktrace span attribute in record…
Vadman97 e75b5a7
fix: fetch the sampling config when LDObserve.start is used without t…
Vadman97 ac160d1
ci: publish package hashes and SLSA provenance for the Java plugin
Vadman97 77e6fff
fix: record an error on the current span when the plugin started it
Vadman97 59f581f
build: compile with options.release instead of source/target compatib…
Vadman97 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' | ||
|
|
||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 . |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.