Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,18 @@ public class Sample {
}
```

### SDK telemetry

By default, the library sends anonymous usage telemetry to Chargebee. This helps us improve the SDK and API.

You can disable this behavior if you prefer:

```java
ChargebeeClient client = ChargebeeClient.builder(apiKey, site)
.sdkTelemetryEnabled(false)
.build();
```

### Telemetry (OpenTelemetry)

Optional. Pass a `telemetryAdapter` when you want Chargebee API calls traced in your observability stack (Datadog, Splunk, Honeycomb, Jaeger, etc.). OpenTelemetry is not bundled with `chargebee-java` — add and configure it in your app, implement `TelemetryAdapter`, and wire it on the client.
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/chargebee/v4/client/ChargebeeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.chargebee.v4.exceptions.TimeoutException;
import com.chargebee.v4.exceptions.TransportException;
import com.chargebee.v4.internal.RetryConfig;
import com.chargebee.v4.telemetry.SdkTelemetryState;
import com.chargebee.v4.telemetry.TelemetryAdapter;
import com.chargebee.v4.telemetry.TelemetryExecutor;
import com.chargebee.v4.transport.*;
Expand Down Expand Up @@ -43,6 +44,8 @@ public final class ChargebeeClient extends ClientMethodsImpl implements AutoClos
private final RequestInterceptor requestInterceptor;
private final RequestContext clientHeaders;
private final TelemetryAdapter telemetryAdapter;
private final boolean sdkTelemetryEnabled;
private final SdkTelemetryState sdkTelemetryState = new SdkTelemetryState();
private final ScheduledExecutorService retryScheduler;

// Auto-generated service registry for lazy loading
Expand All @@ -61,6 +64,7 @@ private ChargebeeClient(Builder builder) {
this.requestInterceptor = builder.requestInterceptor;
this.clientHeaders = new RequestContext(builder.clientHeaders.getHeaders());
this.telemetryAdapter = builder.telemetryAdapter;
this.sdkTelemetryEnabled = builder.sdkTelemetryEnabled;
this.retryScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "chargebee-retry-scheduler");
t.setDaemon(true);
Expand Down Expand Up @@ -97,6 +101,10 @@ public static Builder builder(String apiKey, String siteName) {
public RequestInterceptor getRequestInterceptor() { return requestInterceptor; }
public RequestContext getClientHeaders() { return clientHeaders; }
public TelemetryAdapter getTelemetryAdapter() { return telemetryAdapter; }
public boolean isSdkTelemetryEnabled() { return sdkTelemetryEnabled; }

/** Internal SDK telemetry state; not part of the supported public API. */
public SdkTelemetryState getSdkTelemetryState() { return sdkTelemetryState; }

public String getSdkVersion() {
return getVersion();
Expand Down Expand Up @@ -577,6 +585,7 @@ public static final class Builder {
private String protocol = "https";
private RequestInterceptor requestInterceptor;
private TelemetryAdapter telemetryAdapter;
private boolean sdkTelemetryEnabled = true;
private final RequestContext clientHeaders = new RequestContext();

private Builder() {}
Expand All @@ -600,6 +609,13 @@ public Builder timeout(int connectTimeoutMs, int readTimeoutMs) {
public Builder protocol(String protocol) { this.protocol = protocol; return this; }
public Builder requestInterceptor(RequestInterceptor requestInterceptor) { this.requestInterceptor = requestInterceptor; return this; }
public Builder telemetryAdapter(TelemetryAdapter telemetryAdapter) { this.telemetryAdapter = telemetryAdapter; return this; }
/**
* Enables the anonymous SDK telemetry request header, on by default. It carries SDK name,
* version, runtime, and the resource/operation/latency/status of the previous call on this
* client. It never carries request or response payloads. Pass {@code false} to opt out;
* this is independent of {@link #telemetryAdapter(TelemetryAdapter)}.
*/
public Builder sdkTelemetryEnabled(boolean sdkTelemetryEnabled) { this.sdkTelemetryEnabled = sdkTelemetryEnabled; return this; }

// Header helpers
public Builder header(String name, String value) {
Expand Down
210 changes: 210 additions & 0 deletions src/main/java/com/chargebee/v4/telemetry/SdkTelemetryEmitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* This file is auto-generated by Chargebee.
* For more information on how to make changes to this file, please see the README.
* Reach out to dx@chargebee.com for any questions.
* Copyright 2026 Chargebee Inc.
*/

package com.chargebee.v4.telemetry;

import com.chargebee.v4.client.ChargebeeClient;
import com.chargebee.v4.exceptions.APIException;
import com.chargebee.v4.exceptions.HttpException;
import com.chargebee.v4.transport.DefaultTransport;
import com.chargebee.v4.transport.Request;
import com.chargebee.v4.transport.Response;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Emits the anonymous SDK telemetry request header, independently of any customer telemetry adapter.
*
* <p>Uses an N+1 scheme: the header sent with a call describes the previous completed call on the
* same client, so the first call of a client never carries the header. Every failure path is
* swallowed and logged at {@code WARNING}: telemetry must never fail an API call.
*/
final class SdkTelemetryEmitter {

private static final Logger LOGGER = Logger.getLogger(SdkTelemetryEmitter.class.getName());

private SdkTelemetryEmitter() {}

/** Attaches the header describing the previous call, then records this one for the next. */
static Response around(
ChargebeeClient client, Request request, Function<Request, Response> next) {
if (!client.isSdkTelemetryEnabled()) {
return next.apply(request);
}

long startTimeMs = System.currentTimeMillis();
try {
Response response = next.apply(attachHeader(client, request));
recordSuccess(client, request, response, startTimeMs);
return response;
} catch (RuntimeException err) {
recordFailure(client, request, err, startTimeMs);
throw err;
}
}

/** Async variant of {@link #around}. */
static CompletableFuture<Response> aroundAsync(
ChargebeeClient client,
Request request,
Function<Request, CompletableFuture<Response>> next) {
if (!client.isSdkTelemetryEnabled()) {
return next.apply(request);
}

long startTimeMs = System.currentTimeMillis();
return next.apply(attachHeader(client, request))
.whenComplete(
(response, throwable) -> {
if (throwable != null) {
Throwable cause = throwable.getCause() != null ? throwable.getCause() : throwable;
recordFailure(client, request, cause, startTimeMs);
} else {
recordSuccess(client, request, response, startTimeMs);
}
});
}

/** Returns {@code request} with the telemetry header, or {@code request} unchanged. */
private static Request attachHeader(ChargebeeClient client, Request request) {
try {
SdkTelemetrySnapshot previousCall = client.getSdkTelemetryState().lastCall();
if (previousCall == null) {
return request;
}
String headerValue = SdkTelemetryHeaderBuilder.build(previousCall);
if (headerValue == null) {
return request;
}
return request.withHeader(SdkTelemetryHeader.HEADER_NAME, headerValue);
} catch (Exception err) {
logSuppressed("attach header", err);
return request;
}
}

/** Records a successful call for the next N+1 header. */
private static void recordSuccess(
ChargebeeClient client, Request request, Response response, long startTimeMs) {
if (!request.hasTelemetryMetadata()) {
return;
}
try {
record(
client,
buildSnapshot(
client,
request,
startTimeMs,
response != null ? response.getStatusCode() : null,
null,
extractRequestId(response)));
} catch (Exception err) {
logSuppressed("record success", err);
}
}

/** Records a failed call for the next N+1 header. */
private static void recordFailure(
ChargebeeClient client, Request request, Throwable callError, long startTimeMs) {
if (!request.hasTelemetryMetadata()) {
return;
}
try {
Integer httpStatus = TelemetrySupport.extractHttpStatusCode(callError);
String errorCode = null;
Response response = null;
if (callError instanceof APIException) {
errorCode = ((APIException) callError).getApiErrorCodeRaw();
}
if (callError instanceof HttpException) {
response = ((HttpException) callError).getResponse();
if (httpStatus == null && response != null) {
httpStatus = response.getStatusCode();
}
}
record(
client,
buildSnapshot(
client, request, startTimeMs, httpStatus, errorCode, extractRequestId(response)));
} catch (Exception err) {
logSuppressed("record failure", err);
}
}

/** Stores {@code snapshot} on the client. */
private static void record(ChargebeeClient client, SdkTelemetrySnapshot snapshot) {
client.getSdkTelemetryState().record(snapshot);
}

/** Builds an immutable snapshot of the completed call. */
private static SdkTelemetrySnapshot buildSnapshot(
ChargebeeClient client,
Request request,
long startTimeMs,
Integer httpStatus,
String errorCode,
String requestId) {
return SdkTelemetrySnapshot.builder()
.sdkName(TelemetryAttributeKeys.SDK_NAME)
.sdkVersion(client.getSdkVersion())
.resource(request.getTelemetryResource())
.operation(request.getTelemetryOperation())
.startTimeEpochSeconds(startTimeMs / 1000L)
.timeMs(elapsedMs(startTimeMs))
.httpStatus(httpStatus)
.errorCode(errorCode)
.requestId(requestId)
.featureTokens(resolveFeatureTokens(client, request))
.build();
}

/** Collects {@code ft-*} tokens for the current client/request configuration. */
private static Set<String> resolveFeatureTokens(ChargebeeClient client, Request request) {
Set<String> features = new LinkedHashSet<>();
if (TelemetryAdapterExecutor.resolveAdapter(client, request) != null) {
features.add(SdkTelemetryHeader.FT_TELEMETRY_ADAPTER);
}
if (!(client.getTransport() instanceof DefaultTransport)) {
features.add(SdkTelemetryHeader.FT_CUSTOM_TRANSPORT);
}
if (isRetryConfigActive(client, request)) {
features.add(SdkTelemetryHeader.FT_RETRY_CONFIG);
}
return features;
}

/** Mirrors how {@code sendWithRetryInternal} decides whether retry configuration is in play. */
private static boolean isRetryConfigActive(ChargebeeClient client, Request request) {
if (request.getMaxNetworkRetriesOverride() != null) {
return true;
}
return client.getRetry() != null && client.getRetry().isEnabled();
}

/** Reads {@code chargebee-request-id} from the response, if present. */
private static String extractRequestId(Response response) {
return response != null ? response.getHeader(SdkTelemetryHeader.REQUEST_ID_HEADER) : null;
}

/** Elapsed wall time of the whole call, including any transport-level retries. */
private static long elapsedMs(long startTimeMs) {
return Math.max(0L, System.currentTimeMillis() - startTimeMs);
}

/** Logs a suppressed telemetry failure without affecting the API call. */
private static void logSuppressed(String step, Exception err) {
LOGGER.log(
Level.WARNING,
"SDK telemetry could not " + step + " (" + err.getMessage() + "); API call unaffected.",
err);
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/chargebee/v4/telemetry/SdkTelemetryHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* This file is auto-generated by Chargebee.
* For more information on how to make changes to this file, please see the README.
* Reach out to dx@chargebee.com for any questions.
* Copyright 2026 Chargebee Inc.
*/

package com.chargebee.v4.telemetry;

/** Constants for the anonymous SDK telemetry request header. */
public final class SdkTelemetryHeader {

/**
* Name of the request header carrying SDK telemetry. Exposed so that proxies, interceptors, and
* tests can reference it without hardcoding the string.
*/
public static final String HEADER_NAME = "x-chargebee-sdk-telemetry";

/**
* Server drops larger values, so the SDK omits the header rather than sending a truncated one.
*/
static final int MAX_HEADER_BYTES = 4096;

static final String REQUEST_ID_HEADER = "chargebee-request-id";
static final String RUNTIME = "jvm";
static final String SDK_SEGMENT = "sdk";

static final String FT_TELEMETRY_ADAPTER = "ft-telemetry_adapter";
static final String FT_CUSTOM_TRANSPORT = "ft-custom_transport";
static final String FT_RETRY_CONFIG = "ft-retry_config";

private SdkTelemetryHeader() {}
}
Loading
Loading