Java 21 modules for sending protobuf usage reports to the feddi Platform.
usage-proto: protobuf schema and generated Java contract classes.usage-reporter: GraphQL Java based API usage reporter, reactive client facade, and pluggable reactive HTTP transport API.
feddi-platform depends only on usage-proto. Applications should depend on
usage-reporter, provide their own ReactiveHttpClient implementation, and
configure ApiUsageReporter with a feddi graph variant key via
feddiGraphVariantKey(...). The reporter sends gzipped protobuf requests to
https://feddi.dev by default. Tests and self-hosted deployments can override
the host, but the endpoint paths are fixed to
/api/usage-proto/known-operation-hashes, /api/usage-proto/operations, and
/api/usage-proto/usage.
ApiUsageReporter receives a GraphQL Java Document, operation name, and
GraphQLSchema for each completed API call. It generates an input-aware
canonical operation document with AstSignature, extracts field coordinates,
field argument coordinates, and input object field coordinates, optionally
samples high-throughput traffic, and periodically flushes protobuf batches to
the feddi Platform. Operation definitions are registered separately from usage
events. Each reporter preloads hashes already registered for the graph variant
and keeps an in-memory cache of hashes it registers itself, so repeated requests
send only the hash and request-specific usage metadata.
Add the usage client to the API process that executes GraphQL requests:
dependencies {
implementation 'dev.feddi:feddi-usage-reporter:<release-version>'
}Published versions are listed in the Maven Central repository index at
https://repo1.maven.org/maven2/dev/feddi/. Release versions are timestamps
generated by the feddi Platform release pipeline, for example
2026-05-15-1229; use the newest version listed there unless you need to stay
on a specific feddi Platform release.
The library does not bundle an HTTP implementation. Provide a
ReactiveHttpClient adapter for the HTTP client already used by the host
application. For example, with Spring WebClient:
import dev.feddi.api.usage.http.ReactiveHttpClient;
import dev.feddi.api.usage.http.ReactiveHttpRequest;
import dev.feddi.api.usage.http.ReactiveHttpResponse;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
final class WebClientReactiveHttpClient implements ReactiveHttpClient {
private final WebClient webClient = WebClient.builder().build();
@Override
public Mono<ReactiveHttpResponse> exchange(ReactiveHttpRequest request) {
var spec = webClient
.method(HttpMethod.valueOf(request.method()))
.uri(request.uri());
request.headers().forEach((name, values) ->
values.forEach(value -> spec.header(name, value)));
return spec.body(request.body(), byte[].class)
.exchangeToMono(response -> response.bodyToMono(byte[].class)
.defaultIfEmpty(new byte[0])
.map(body -> new ReactiveHttpResponse(
response.statusCode().value(),
response.headers().asHttpHeaders(),
Mono.just(body))));
}
}Create one reporter for the process and reuse it for all requests that belong to the configured graph variant:
import dev.feddi.api.usage.ApiUsageReporter;
var reporter = ApiUsageReporter.builder(new WebClientReactiveHttpClient())
.feddiGraphVariantKey(System.getenv("FEDDI_GRAPH_VARIANT_KEY"))
.build();The default host is https://feddi.dev. Self-hosted deployments and tests can
override only the host; the operation-registration and usage-ingestion paths
remain fixed:
var reporter = ApiUsageReporter.builder(httpClient)
.feddiGraphVariantKey(feddiGraphVariantKey)
.host("https://platform.example.com")
.build();Report each completed GraphQL request after execution. The Document should be
the parsed GraphQL Java document for the request, and the GraphQLSchema
should be the executable schema used to run it.
Pass the runtime variables map when the operation used variables. This lets the reporter detect which optional input object fields were actually present in the request. Inline input object literals are analyzed from the GraphQL document.
import dev.feddi.api.usage.ApiUsageInvocation;
long startedAt = System.nanoTime();
// Execute the GraphQL request with your GraphQL Java runtime.
boolean queued = reporter.report(ApiUsageInvocation.builder()
.document(document)
.operationName(operationName)
.schema(graphQLSchema)
.variables(variables)
.durationNanos(System.nanoTime() - startedAt)
.httpError(httpStatusCode >= 500)
.graphqlError(!executionResult.getErrors().isEmpty())
.clientName("orders-api")
.clientVersion("1.0.0")
.build());report(...) is non-blocking. It returns false when the reporter is closed,
the event was sampled out after adaptive sampling was explicitly enabled, or
the in-memory queue is full.
Flush and close the reporter during application shutdown:
reporter.close();Reactive hosts can use closeAsync() instead:
reporter.closeAsync().subscribe();The reporter uses three protobuf endpoints:
/api/usage-proto/known-operation-hashesfetches operation hashes already registered for the graph variant so new reporters can avoid duplicate operation-definition uploads./api/usage-proto/operationsregisters operation hashes, canonical documents, field coordinates, and input usage coordinates./api/usage-proto/usageingests request usage events by operation hash.
Request bodies are gzip-compressed and sent with
Content-Encoding: gzip.
feddiGraphVariantKey(...)is required and is used as the bearer token.host(...)is optional. It must be an absolute host URI with no path other than an optional trailing slash, query, or fragment.batchWindow(min, max)controls the randomized scheduled background flush window. Each background flush samples a new delay between the two durations. The default window is 20-40 seconds.- Each flush sends all usage records that are queued when draining starts.
maxQueueSize(...)controls the pending in-memory queue size and therefore bounds the largest automatic flush request. The default is 22,222 records, derived from a 1 MB compressed request budget and about 45 compressed bytes per usage record. The absolute maximum is 44,444 records, derived from a 2 MB compressed request budget. Lower values can be configured; higher values are rejected.samplingEnabled(...)controls adaptive sampling and defaults tofalse.flushErrorHandler(...)receives background flush failures and per-record analysis failures.
When adaptive sampling is enabled, sampling is recalculated on every flush from the request count observed during the batch window. Traffic below 100 requests per second sends every event. Higher traffic is sampled and sent with a multiplier so aggregate counts remain representative.