Skip to content

fix: share single Telemetry instance per SDK client#290

Open
jimmyjames wants to merge 2 commits intomainfrom
fix/209-share-telemetry-instance
Open

fix: share single Telemetry instance per SDK client#290
jimmyjames wants to merge 2 commits intomainfrom
fix/209-share-telemetry-instance

Conversation

@jimmyjames
Copy link
Contributor

@jimmyjames jimmyjames commented Feb 15, 2026

Description

Fixes #209

The SDK was creating new Telemetry, Metrics, and associated OpenTelemetry instruments for every HTTP request instead of sharing a single instance per SDK client. This caused:

  • Wasted object allocation on every request
  • Fragmented OTel instrument instances (potential metric aggregation issues)
  • Repeated GlobalOpenTelemetry meter lookups

Additionally, once a single Telemetry/Metrics instance is shared across concurrent async requests, the existing code had thread-safety bugs that are fixed here.

Changes

Thread safety (Telemetry.java, Metrics.java):

  • Telemetry.metrics(): volatile field + double-checked locking for safe lazy init
  • Metrics: HashMapConcurrentHashMap, check-then-put → computeIfAbsent() for counters and histograms

Shared instance wiring (OpenFgaClient.java, OpenFgaApi.java, ApiExecutor.java, OAuth2Client.java, HttpRequestAttempt.java):

  • OpenFgaClient owns the single Telemetry instance, passes it to OpenFgaApi and ApiExecutor
  • OpenFgaApi passes shared telemetry through to all HttpRequestAttempt calls
  • OAuth2Client passes its own Telemetry (one per client, not per request) to HttpRequestAttempt
  • New constructors validate telemetry parameter for null, consistent with existing parameter validation

Backward compatibility:

  • All existing public constructors are preserved as overloads that delegate to new versions
  • HttpRequestAttempt(5-param) delegates to new HttpRequestAttempt(6-param)
  • ApiExecutor(2-param) delegates to new ApiExecutor(3-param)
  • OpenFgaApi(1-param) and OpenFgaApi(2-param) preserved, new OpenFgaApi(3-param) added
  • No breaking changes

Test plan

  • Existing TelemetryTest.shouldBeASingletonMetricsInitialization validates singleton behavior
  • New concurrent access test verifies Telemetry.metrics() returns same instance across 10 threads
  • New test for backward-compatible ApiExecutor 2-param constructor
  • HttpRequestAttemptRetryTest updated to use shared telemetry instances
  • ./gradlew check passes (tests + spotless formatting)
  • CI passes on Java 17, 21, 25

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for injecting custom telemetry instances throughout the SDK, enabling greater flexibility in telemetry configuration.
  • Improvements

    • Enhanced thread-safety with concurrent data structures and double-checked locking for singleton initialization.
    • Strengthened input validation to ensure telemetry instances are non-null during initialization.
  • Tests

    • Added comprehensive test coverage for null telemetry validation and concurrent access scenarios.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 15, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR addresses issue #209 by introducing a single Telemetry instance per SDK instance instead of creating new ones for each HTTP request. New constructors accepting Telemetry are added to OpenFgaApi, ApiExecutor, and HttpRequestAttempt, while existing constructors delegate to these new ones. Thread-safety improvements are applied to Telemetry initialization and Metrics data structures.

Changes

Cohort / File(s) Summary
Telemetry Constructor Injection
src/main/java/dev/openfga/sdk/api/OpenFgaApi.java, src/main/java/dev/openfga/sdk/api/client/ApiExecutor.java, src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java, src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java, src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java
New constructors accepting Telemetry parameter added to key classes; existing constructors delegate to new ones passing new Telemetry(configuration). HttpRequestAttempt invocations updated to pass telemetry instance for propagation through HTTP request chain.
Thread-Safety Improvements
src/main/java/dev/openfga/sdk/telemetry/Telemetry.java, src/main/java/dev/openfga/sdk/telemetry/Metrics.java
ConcurrentHashMap replaces HashMap for thread-safe counter and histogram access; computeIfAbsent simplifies lazy initialization. Telemetry configuration field marked final; metrics field marked volatile with double-checked locking pattern for thread-safe singleton initialization.
Test Coverage
src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java, src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java, src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java, src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java
New tests validate null telemetry rejection in constructors; concurrent access test verifies singleton behavior under multithreaded conditions. HttpRequestAttemptRetryTest updated to pass telemetry in constructor calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • rhamzeh
  • evansims
🚥 Pre-merge checks | ✅ 5 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: sharing a single Telemetry instance per SDK client instead of creating new instances for each request.
Linked Issues check ✅ Passed The PR fully addresses the requirements of issue #209 by eliminating per-request Telemetry creation and implementing a single shared instance per SDK client with proper thread-safety mechanisms.
Out of Scope Changes check ✅ Passed All changes directly support the objective of sharing a single Telemetry instance: constructor wiring, thread-safety improvements (ConcurrentHashMap, volatile fields, double-checked locking), and comprehensive testing.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/209-share-telemetry-instance

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link

codecov-commenter commented Feb 15, 2026

Codecov Report

❌ Patch coverage is 98.24561% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 37.80%. Comparing base (d4b4f73) to head (34cfbe2).

Files with missing lines Patch % Lines
...main/java/dev/openfga/sdk/telemetry/Telemetry.java 88.88% 0 Missing and 1 partial ⚠️

❌ Your project status has failed because the head coverage (37.80%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #290      +/-   ##
============================================
+ Coverage     37.70%   37.80%   +0.09%     
- Complexity     1236     1243       +7     
============================================
  Files           197      197              
  Lines          7609     7621      +12     
  Branches        880      883       +3     
============================================
+ Hits           2869     2881      +12     
  Misses         4601     4601              
  Partials        139      139              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java (1)

484-485: ⚠️ Potential issue | 🟡 Minor

Missing telemetry parameter - inconsistent with other tests.

The shouldRetryOnConnectionTimeout test uses the old 5-parameter HttpRequestAttempt constructor without passing Telemetry, while all other tests in this file were updated to use the new 6-parameter constructor with telemetry. This appears to be an oversight.

🔧 Proposed fix
         HttpRequestAttempt<Void> attempt =
-                new HttpRequestAttempt<>(request, "test", Void.class, apiClient, timeoutConfig);
+                new HttpRequestAttempt<>(request, "test", Void.class, apiClient, timeoutConfig, new Telemetry(timeoutConfig));
🤖 Fix all issues with AI agents
In `@src/main/java/dev/openfga/sdk/api/OpenFgaApi.java`:
- Around line 83-87: The OpenFgaApi constructor OpenFgaApi(Configuration,
ApiClient, Telemetry) lacks a null check for telemetry; add a validation at the
start of that constructor that throws IllegalArgumentException if telemetry is
null (matching ApiExecutor behavior) so subsequent uses of telemetry (e.g., in
HttpRequestAttempt calls) cannot cause a NullPointerException.
🧹 Nitpick comments (2)
src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java (1)

385-391: Test verifies backward compatibility but could be more robust.

The test confirms the 2-parameter constructor works without throwing, which validates backward compatibility. However, consider expanding it to verify the internally-created Telemetry is functional:

💡 Optional: Add verification that the executor is functional
 `@Test`
 public void twoParamConstructor_shouldCreateWithOwnTelemetry() throws Exception {
     // Verifies the backward-compatible 2-param constructor works
     ClientConfiguration config = new ClientConfiguration().apiUrl(fgaApiUrl).storeId(DEFAULT_STORE_ID);
     ApiExecutor executor = new ApiExecutor(new ApiClient(), config);
     assertNotNull(executor);
+    // Optionally verify it can build a request (doesn't require actual HTTP call)
+    assertNotNull(executor.getClass().getDeclaredField("telemetry"));
 }
src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java (1)

80-84: Consider thread-safety for concurrent configuration updates.

When setConfiguration is called, both telemetry and api fields are reassigned in two separate statements (lines 82-83). If OpenFgaClient is used concurrently from multiple threads, a thread could observe an inconsistent state where telemetry is updated but api still references the old OpenFgaApi (or vice versa).

If concurrent access to OpenFgaClient is expected, consider either:

  1. Making the updates atomic using synchronization
  2. Documenting that setConfiguration is not thread-safe and should not be called concurrently with API operations

Instead of creating new Telemetry, Metrics, and OpenTelemetry instruments
for every HTTP request, share a single Telemetry instance per SDK client.
This avoids wasted allocation, fragmented OTel instrument instances, and
repeated GlobalOpenTelemetry meter lookups.

Also fixes thread-safety bugs that become relevant once the instance is
shared across concurrent async requests:
- Telemetry.metrics(): volatile field + double-checked locking
- Metrics counters/histograms: HashMap → ConcurrentHashMap with computeIfAbsent

All existing public constructors are preserved for backward compatibility.

Closes #209
@jimmyjames jimmyjames force-pushed the fix/209-share-telemetry-instance branch from b5251de to d2058e8 Compare February 15, 2026 21:27
@jimmyjames jimmyjames marked this pull request as ready for review February 15, 2026 21:32
@jimmyjames jimmyjames requested a review from a team as a code owner February 15, 2026 21:32
Copilot AI review requested due to automatic review settings February 15, 2026 21:32
@dosubot
Copy link

dosubot bot commented Feb 15, 2026

Related Documentation

Checked 8 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes SDK telemetry/metrics being re-created per HTTP request by introducing a shared Telemetry instance per SDK client and making Telemetry/Metrics safe for concurrent use, addressing issue #209.

Changes:

  • Share a single Telemetry instance across request execution paths (OpenFgaClientOpenFgaApi/ApiExecutorHttpRequestAttempt) and within OAuth2Client.
  • Make lazy Telemetry.metrics() initialization thread-safe (double-checked locking) and make Metrics instrument caches concurrent (ConcurrentHashMap + computeIfAbsent).
  • Add/adjust tests for singleton behavior, concurrent access, constructor compatibility, and null telemetry validation.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/main/java/dev/openfga/sdk/telemetry/Telemetry.java Thread-safe lazy initialization of a shared Metrics instance.
src/main/java/dev/openfga/sdk/telemetry/Metrics.java Make counter/histogram caches concurrency-safe and atomically initialized.
src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java Own and propagate a single Telemetry instance per client.
src/main/java/dev/openfga/sdk/api/OpenFgaApi.java Accept and reuse injected Telemetry for all request attempts; add null validation.
src/main/java/dev/openfga/sdk/api/client/ApiExecutor.java Accept and reuse injected Telemetry; preserve 2-param constructor via delegation.
src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java Add telemetry-injecting constructor and enforce non-null telemetry.
src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java Reuse the client’s own telemetry when issuing token exchange requests.
src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java Add concurrent access test for Telemetry.metrics() singleton behavior.
src/test/java/dev/openfga/sdk/api/client/HttpRequestAttemptRetryTest.java Update call sites to pass telemetry; add null telemetry rejection test.
src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java Add tests for backward-compatible constructor and null telemetry validation.
src/test/java/dev/openfga/sdk/api/OpenFgaApiTest.java Add null telemetry rejection test for new constructor overload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 36 to 54
// when
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
startLatch.await();
results.add(telemetry.metrics());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
startLatch.countDown();
executor.shutdown();
executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);

// then
assertThat(results).hasSize(threadCount);
Metrics expected = results.get(0);
assertThat(results).allSatisfy(m -> assertThat(m).isSameAs(expected));
Copy link

Copilot AI Feb 15, 2026

Choose a reason for hiding this comment

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

The test doesn't verify that awaitTermination(...) actually completed and doesn't ensure the ExecutorService is cleaned up if the test fails early. This can lead to flaky assertions (results not fully populated) and potential thread leakage in CI. Consider asserting the awaitTermination result and using a try/finally to call shutdownNow() when needed.

Suggested change
// when
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
startLatch.await();
results.add(telemetry.metrics());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
startLatch.countDown();
executor.shutdown();
executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
// then
assertThat(results).hasSize(threadCount);
Metrics expected = results.get(0);
assertThat(results).allSatisfy(m -> assertThat(m).isSameAs(expected));
try {
// when
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
startLatch.await();
results.add(telemetry.metrics());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
startLatch.countDown();
executor.shutdown();
boolean terminated = executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
assertThat(terminated).isTrue();
// then
assertThat(results).hasSize(threadCount);
Metrics expected = results.get(0);
assertThat(results).allSatisfy(m -> assertThat(m).isSameAs(expected));
} finally {
if (!executor.isTerminated()) {
executor.shutdownNow();
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 34cfbe2 — added try/finally with shutdownNow() cleanup and an assertion on the awaitTermination return value. Thanks for the catch!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/test/java/dev/openfga/sdk/telemetry/TelemetryTest.java (1)

49-49: Nit: import TimeUnit instead of using FQN.

Lines 6–10 already import other java.util.concurrent types. For consistency, import TimeUnit as well.

 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;

Then on line 49:

-        executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
+        executor.awaitTermination(5, TimeUnit.SECONDS);

@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Feb 16, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

Assert awaitTermination returns true for clear timeout failures, and
wrap test body in try/finally with shutdownNow to prevent thread leaks.
@jimmyjames jimmyjames force-pushed the fix/209-share-telemetry-instance branch from 289a4ae to 34cfbe2 Compare February 16, 2026 15:47
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.

Unnecessary Telemetry Objects Created

2 participants