Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
### v4.12.0 (2026-06-30)
* * *

### New Features
* Added an optional `telemetryAdapter` hook for tracing Chargebee API calls via OpenTelemetry (or any APM). Configure it once on the client (`ChargebeeClient.Builder#telemetryAdapter`) or per call (`RequestOptions.Builder#telemetryAdapter`). When unconfigured, the SDK skips all telemetry work — no behavior change for existing integrations.
* Each API call emits one CLIENT span (`chargebee.{resource}.{operation}`) with OpenTelemetry HTTP semantic-convention attributes (`url.full`, `http.request.method`, `server.address`, `http.response.status_code`) plus `chargebee.*` attributes. Adapters may inject W3C trace context (`traceparent`, `tracestate`) into outbound request headers for distributed tracing.
* Exposed the `TelemetryAdapter`, `RequestTelemetryContext`, `RequestTelemetryResult`, `RequestTelemetryError` types, the `TelemetrySupport` helpers, and the `TelemetryAttributeKeys` constants under `com.chargebee.v4.telemetry`.
* Added a convenience `updateGift(String giftId)` overload (and its `updateGiftAsync` async variant) to `GiftService` for invoking `update_gift` without params.



### v4.11.0 (2026-06-29)
* * *
### Bug Fixes:
Expand Down
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,139 @@ public class Sample {
}
```

### 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.

The SDK builds standardized span attributes (`ctx.getStartAttributes()`, `result.getEndAttributes()`) following the stable [OpenTelemetry HTTP semantic conventions](https://opentelemetry.io/docs/specs/semconv/http/http-spans/) (`url.full`, `http.request.method`, `http.response.status_code`, `server.address`, `error.type`) plus Chargebee-specific `chargebee.*` attributes — use them as-is so spans render correctly in your APM and stay consistent across SDKs.

Spans are named `chargebee.{resource}.{operation}` (e.g. `chargebee.subscription.create`).

#### OpenTelemetry example

```kotlin
dependencies {
implementation("io.opentelemetry:opentelemetry-api:1.49.0")
implementation("io.opentelemetry:opentelemetry-sdk:1.49.0")
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.49.0")
}
```

Configure OpenTelemetry at app startup, then pass your adapter:

```java
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;

// App startup — configure once
OtlpGrpcSpanExporter spanExporter =
OtlpGrpcSpanExporter.builder()
.setEndpoint(System.getenv().getOrDefault("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"))
.build();

SdkTracerProvider tracerProvider =
SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
.setResource(
Resource.getDefault()
.merge(
Resource.create(
Attributes.of(AttributeKey.stringKey("service.name"), "billing-service"))))
.build();

OpenTelemetry openTelemetry =
OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal();
```

```java
import com.chargebee.v4.client.ChargebeeClient;
import com.chargebee.v4.telemetry.RequestTelemetryContext;
import com.chargebee.v4.telemetry.RequestTelemetryResult;
import com.chargebee.v4.telemetry.TelemetryAdapter;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import java.util.Map;

class OtelTelemetryAdapter implements TelemetryAdapter {
private final OpenTelemetry openTelemetry;
private final Tracer tracer;

OtelTelemetryAdapter(OpenTelemetry openTelemetry) {
this.openTelemetry = openTelemetry;
this.tracer = openTelemetry.getTracer("chargebee-java");
}

@Override
public Object onRequestStart(RequestTelemetryContext ctx, Map<String, String> requestHeaders) {
AttributesBuilder attrs = Attributes.builder();
ctx.getStartAttributes().forEach((k, v) -> attrs.put(AttributeKey.stringKey(k), v));

Span span =
tracer
.spanBuilder(ctx.getSpanName())
.setSpanKind(SpanKind.CLIENT)
.setAllAttributes(attrs.build())
.startSpan();

Context context = Context.current().with(span);
openTelemetry
.getPropagators()
.getTextMapPropagator()
.inject(context, requestHeaders, (carrier, key, value) -> carrier.put(key, value));

return span;
}

@Override
public void onRequestEnd(Object handle, RequestTelemetryResult result) {
if (!(handle instanceof Span)) {
return;
}
Span span = (Span) handle;
result
.getEndAttributes()
.forEach(
(k, v) -> {
if (v instanceof String) {
span.setAttribute(k, (String) v);
} else if (v instanceof Long) {
span.setAttribute(k, (Long) v);
} else if (v instanceof Integer) {
span.setAttribute(k, ((Integer) v).longValue());
}
});
if (result.getError() != null) {
span.setStatus(StatusCode.ERROR, result.getError().getMessage());
} else {
span.setStatus(StatusCode.OK);
}
span.end();
}
}

ChargebeeClient client =
ChargebeeClient.builder()
.apiKey("{{api-key}}")
.siteName("{{site}}")
.telemetryAdapter(new OtelTelemetryAdapter(openTelemetry))
.build();
```

Spans are exported by your own OpenTelemetry setup, so they flow to whatever backend you've configured (Datadog, Splunk, Honeycomb, Jaeger, etc.). The Chargebee config above stays the same regardless of backend — refer to your APM vendor's OpenTelemetry/OTLP documentation for exporter endpoints.

## Features

### SDK Features
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.11.0
4.12.0
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "com.chargebee"
version = "4.11.0"
version = "4.12.0"
description = "Java client library for ChargeBee"

// Project metadata
Expand Down
29 changes: 29 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,8 @@
import com.chargebee.v4.exceptions.TimeoutException;
import com.chargebee.v4.exceptions.TransportException;
import com.chargebee.v4.internal.RetryConfig;
import com.chargebee.v4.telemetry.TelemetryAdapter;
import com.chargebee.v4.telemetry.TelemetryExecutor;
import com.chargebee.v4.transport.*;
import com.chargebee.v4.transport.Transport;

Expand Down Expand Up @@ -40,6 +42,7 @@ public final class ChargebeeClient extends ClientMethodsImpl implements AutoClos
private final String protocol;
private final RequestInterceptor requestInterceptor;
private final RequestContext clientHeaders;
private final TelemetryAdapter telemetryAdapter;
private final ScheduledExecutorService retryScheduler;

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

public String getSdkVersion() {
return getVersion();
}

@Override
public void close() {
Expand Down Expand Up @@ -303,6 +312,10 @@ public CompletableFuture<Response> executeWithInterceptorAsync(Request request)
* Send a request with retry logic based on the configured RetryConfig.
*/
public Response sendWithRetry(Request request) {
return TelemetryExecutor.execute(this, request, this::sendWithRetryInternal);
}

private Response sendWithRetryInternal(Request request) {
Request enrichedRequest = addDefaultHeaders(request);

Integer overrideRetries = enrichedRequest.getMaxNetworkRetriesOverride();
Expand Down Expand Up @@ -370,6 +383,16 @@ private Request addDefaultHeaders(Request request) {
builder.followRedirectsOverride(request.getFollowRedirectsOverride());
}

if (request.getTelemetryResource() != null) {
builder.telemetryResource(request.getTelemetryResource());
}
if (request.getTelemetryOperation() != null) {
builder.telemetryOperation(request.getTelemetryOperation());
}
if (request.getTelemetryAdapterOverride() != null) {
builder.telemetryAdapterOverride(request.getTelemetryAdapterOverride());
}

addStandardHeaders(builder);

for (Map.Entry<String, String> header : request.getHeaders().entrySet()) {
Expand Down Expand Up @@ -456,6 +479,10 @@ private long calculateBackoffDelay(int attempt) {
* Send a request asynchronously with retry logic based on the configured RetryConfig.
*/
public CompletableFuture<Response> sendWithRetryAsync(Request request) {
return TelemetryExecutor.executeAsync(this, request, this::sendWithRetryAsyncInternal);
}

private CompletableFuture<Response> sendWithRetryAsyncInternal(Request request) {
Request enrichedRequest = addDefaultHeaders(request);

Integer overrideRetries = enrichedRequest.getMaxNetworkRetriesOverride();
Expand Down Expand Up @@ -549,6 +576,7 @@ public static final class Builder {
private String domainSuffix = "chargebee.com";
private String protocol = "https";
private RequestInterceptor requestInterceptor;
private TelemetryAdapter telemetryAdapter;
private final RequestContext clientHeaders = new RequestContext();

private Builder() {}
Expand All @@ -571,6 +599,7 @@ public Builder timeout(int connectTimeoutMs, int readTimeoutMs) {
public Builder domainSuffix(String domainSuffix) { this.domainSuffix = domainSuffix; return this; }
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; }

// Header helpers
public Builder header(String name, String value) {
Expand Down
24 changes: 19 additions & 5 deletions src/main/java/com/chargebee/v4/client/request/RequestOptions.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.chargebee.v4.client.request;

import com.chargebee.v4.transport.RequestLogger;
import com.chargebee.v4.telemetry.TelemetryAdapter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
Expand All @@ -21,6 +22,7 @@ public final class RequestOptions {
private final Boolean followRedirects;
private final Boolean gzipCompression;
private final RequestLogger requestLogger;
private final TelemetryAdapter telemetryAdapter;

private RequestOptions(
Map<String, String> headers,
Expand All @@ -32,7 +34,8 @@ private RequestOptions(
Integer readTimeoutMs,
Boolean followRedirects,
Boolean gzipCompression,
RequestLogger requestLogger
RequestLogger requestLogger,
TelemetryAdapter telemetryAdapter
) {
// Java 8 compatibility - use HashMap constructor instead of Map.copyOf
this.headers = new HashMap<>(headers);
Expand All @@ -45,10 +48,11 @@ private RequestOptions(
this.followRedirects = followRedirects;
this.gzipCompression = gzipCompression;
this.requestLogger = requestLogger;
this.telemetryAdapter = telemetryAdapter;
}

public static RequestOptions empty() {
return new RequestOptions(new HashMap<>(), null, null, null, null, null, null, null, null, null);
return new RequestOptions(new HashMap<>(), null, null, null, null, null, null, null, null, null, null);
}

public static Builder builder() {
Expand All @@ -58,13 +62,13 @@ public static Builder builder() {
public RequestOptions withHeader(String key, String value) {
Map<String, String> copy = new HashMap<>(headers);
copy.put(key, value);
return new RequestOptions(copy, maxNetworkRetries, retryEnabled, retryBaseDelayMs, retryOnStatus, connectTimeoutMs, readTimeoutMs, followRedirects, gzipCompression, requestLogger);
return new RequestOptions(copy, maxNetworkRetries, retryEnabled, retryBaseDelayMs, retryOnStatus, connectTimeoutMs, readTimeoutMs, followRedirects, gzipCompression, requestLogger, telemetryAdapter);
}

public RequestOptions withHeaders(Map<String, String> newHeaders) {
Map<String, String> copy = new HashMap<>(headers);
copy.putAll(newHeaders);
return new RequestOptions(copy, maxNetworkRetries, retryEnabled, retryBaseDelayMs, retryOnStatus, connectTimeoutMs, readTimeoutMs, followRedirects, gzipCompression, requestLogger);
return new RequestOptions(copy, maxNetworkRetries, retryEnabled, retryBaseDelayMs, retryOnStatus, connectTimeoutMs, readTimeoutMs, followRedirects, gzipCompression, requestLogger, telemetryAdapter);
}

public Map<String, String> getHeaders() {
Expand Down Expand Up @@ -116,6 +120,10 @@ public RequestLogger getRequestLogger() {
return requestLogger;
}

public TelemetryAdapter getTelemetryAdapter() {
return telemetryAdapter;
}

public static final class Builder {
private final Map<String, String> headers = new HashMap<>();
private Integer maxNetworkRetries;
Expand All @@ -127,6 +135,7 @@ public static final class Builder {
private Boolean followRedirects;
private Boolean gzipCompression;
private RequestLogger requestLogger;
private TelemetryAdapter telemetryAdapter;

public Builder header(String name, String value) {
if (name != null && value != null) {
Expand Down Expand Up @@ -222,8 +231,13 @@ public Builder requestLogger(RequestLogger requestLogger) {
return this;
}

public Builder telemetryAdapter(TelemetryAdapter telemetryAdapter) {
this.telemetryAdapter = telemetryAdapter;
return this;
}

public RequestOptions build() {
return new RequestOptions(headers, maxNetworkRetries, retryEnabled, retryBaseDelayMs, retryOnStatus, connectTimeoutMs, readTimeoutMs, followRedirects, gzipCompression, requestLogger);
return new RequestOptions(headers, maxNetworkRetries, retryEnabled, retryBaseDelayMs, retryOnStatus, connectTimeoutMs, readTimeoutMs, followRedirects, gzipCompression, requestLogger, telemetryAdapter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,19 @@ public AdditionalBillingLogiqService withOptions(RequestOptions options) {
*/
Response retrieveRaw(AdditionalBillingLogiqRetrieveParams params) throws ChargebeeException {

return get("/additional_billing_logiqs", params != null ? params.toQueryParams() : null);
return get(
"additionalBillingLogiq",
"retrieve",
"/additional_billing_logiqs",
params != null ? params.toQueryParams() : null);
}

/**
* retrieve a additionalBillingLogiq without params (executes immediately) - returns raw Response.
*/
Response retrieveRaw() throws ChargebeeException {

return get("/additional_billing_logiqs", null);
return get("additionalBillingLogiq", "retrieve", "/additional_billing_logiqs", null);
}

/**
Expand All @@ -90,7 +94,11 @@ public AdditionalBillingLogiqRetrieveResponse retrieve(
public CompletableFuture<AdditionalBillingLogiqRetrieveResponse> retrieveAsync(
AdditionalBillingLogiqRetrieveParams params) {

return getAsync("/additional_billing_logiqs", params != null ? params.toQueryParams() : null)
return getAsync(
"additionalBillingLogiq",
"retrieve",
"/additional_billing_logiqs",
params != null ? params.toQueryParams() : null)
.thenApply(
response ->
AdditionalBillingLogiqRetrieveResponse.fromJson(
Expand All @@ -106,7 +114,7 @@ public AdditionalBillingLogiqRetrieveResponse retrieve() throws ChargebeeExcepti
/** Async variant of retrieve for additionalBillingLogiq without params. */
public CompletableFuture<AdditionalBillingLogiqRetrieveResponse> retrieveAsync() {

return getAsync("/additional_billing_logiqs", null)
return getAsync("additionalBillingLogiq", "retrieve", "/additional_billing_logiqs", null)
.thenApply(
response ->
AdditionalBillingLogiqRetrieveResponse.fromJson(
Expand Down
Loading
Loading