Java Client
A thin library designed to publish messages to Hermes.
Features
- http client-agnostic API
- synchronous/asynchronous publishing
- configurable retries
- metrics
Overview
Core functionality is provided by HermesClient class, which in turn uses HermesSender to do the heavy lifting.
At the moment there are four implementations of HermesSender:
- RestTemplateHermesSender - recommended for services built on Spring framework; uses AsyncRestTemplate for asynchronous transmission
- WebClientHermesSender - for services using Spring WebFlux; uses WebClient
- JerseyHermesSender - recommended for services using Jersey
- OkHttpHermesSender - supports both HTTP/1.1 and HTTP/2 protocols, uses OkHttp3 client
Creating
To start using HermesClient, add it as an dependency:
compile group: 'pl.allegro.tech.hermes', name: 'hermes-client', version: versions.hermes
Client should be always built using HermesClientBuilder, which allows on setting:
HermesClient client = HermesClientBuilder.hermesClient(...)
.withURI(...) // Hermes URI
.withRetries(...) // how many times retry in case of errors, default: 3
.withRetrySleep(...) // initial and max delay between consecutive retries in milliseconds, default: 100ms (initial), 300ms (max)
.withDefaultContentType(...) // what Content-Type to use when none set, default: application/json
.withDefaultHeaderValue(...) // append default headers added to each message
.withMetrics(metricsRegistry) // see Metrics section below
.build();
See Sender implementations sections for guides on how to construct HermesSender.
Once created, you can start publishing messages. Hermes Client API is asynchronous by default, returning
CompletableFuture object that holds the promise of a result.
HermesClient exposes methods for easy publication of JSON and Avro messages.
JSON sender sets application/json content type.
hermesClient.publishJSON("com.group.json", "{hello: 1}");
Avro sender sets avro/binary content type. It also requires to pass Avro schema version of this message, which is
passed on to Hermes in Schema-Version header.
hermesClient.publishAvro("com.group.avro",1,avroMessage.getBytes());
You can also use HermesMessage#Builder to create HermesMessage object, to e.g. pass custom headers:
hermesClient.publish(
HermesMessage.hermesMessage("com.group.topic", "{hello: 1}")
.json()
.withHeader("My-Header", "header value")
.build()
);
Publication results in returning HermesResponse object:
CompletableFuture<HermesResponse> result = client.publish("group.topic", "{}");
HermesResponse response = result.join();
assert response.isSuccess();
assert response.getStatusCode() == 201;
assert response.getMessageId().equals("..."); // message UUID generated by Hermes
Closing
The client allows graceful shutdown, which causes it to stop accepting publish requests and await for delivery of currently processed messages.
Two variants of shutting down the client are available:
- synchronous
void close(long pollInterval, long timeout)method will return when all currently processed messages (including retries) are delivered or discarded - asynchronous
CompletableFuture<Void> closeAsync(long pollInterval)returns immediately and the returned CompletableFuture completes when all messages are delivered or discarded
pollInterval parameter is used for polling the internal counter of asynchronously processed messages with the user specified interval in milliseconds.
Calls to publish methods will return an exceptionally completed future with HermesClientShutdownException:
client.close(50, 1000);
assert client.publish("group.topic", "{}").isCompletedExceptionally();
One can use the asynchronous method to wait for the client to terminate within a given timeout, e.g. in a shutdown hook:
client.closeAsync(50).get(1, TimeUnit.SECONDS);
Metrics
Hermes Client reports publishing metrics under the hermes-client. prefix. All metrics are tagged with topic.
Setup
Micrometer (recommended)
MeterRegistry meterRegistry = ... // your MeterRegistry (e.g. PrometheusMeterRegistry)
MetricsProvider metricsProvider = new MicrometerTaggedMetricsProvider(meterRegistry);
HermesClient client = HermesClientBuilder.hermesClient(sender)
.withURI(URI.create("http://localhost:8080"))
.withMetrics(metricsProvider)
.build();
Dropwizard
Requirement: dependency io.dropwizard.metrics:metrics-core must be provided at runtime.
MetricRegistry registry = new MetricRegistry();
HermesClient client = HermesClientBuilder.hermesClient(sender)
.withURI(URI.create("http://localhost:8080"))
.withMetrics(registry)
.build();
Metrics reference
The table below lists all metrics reported by the Hermes Client. The Prometheus name column shows
the metric name as it appears in Prometheus after standard Micrometer naming conversion (dots and hyphens
become underscores, counters get the _total suffix, timers get _seconds suffix).
Counters
| Micrometer name | Prometheus name | Tags | Description |
|---|---|---|---|
hermes-client.status |
hermes_client_status_total |
topic, code |
Total number of HTTP responses received from Hermes, partitioned by HTTP status code. |
hermes-client.publish.attempt |
hermes_client_publish_attempt_total |
topic |
Total number of messages for which the publish process has completed (either successfully or after exhausting all retries). |
hermes-client.publish.failure |
hermes_client_publish_failure_total |
topic |
Total number of individual publish attempts that received a failure (non-2xx) HTTP response. |
hermes-client.publish.finally.success |
hermes_client_publish_finally_success_total |
topic |
Total number of messages that were ultimately published successfully (with or without retries). |
hermes-client.publish.finally.failure |
hermes_client_publish_finally_failure_total |
topic |
Total number of messages that ultimately failed to be published (after all retries were exhausted or a non-2xx response was final). |
hermes-client.publish.retry.attempt |
hermes_client_publish_retry_attempt_total |
topic |
Total number of messages for which at least one retry was attempted, regardless of the final outcome. |
hermes-client.publish.retry.success |
hermes_client_publish_retry_success_total |
topic |
Total number of messages that were published successfully after at least one retry. |
hermes-client.publish.retry.failure |
hermes_client_publish_retry_failure_total |
topic |
Total number of individual retry attempts that resulted in a failure response. |
hermes-client.failure |
hermes_client_failure_total |
topic |
Total number of failed publish attempts (exceptions or non-2xx responses), including each failed retry. |
hermes-client.retries.count |
hermes_client_retries_count_total |
topic |
Total number of retry attempts triggered by a failed previous attempt (exception or response matching retry condition). |
hermes-client.retries.success |
hermes_client_retries_success_total |
topic |
Total number of messages for which the publish completed without exhausting all retries (including first-attempt successes). |
hermes-client.retries.exhausted |
hermes_client_retries_exhausted_total |
topic |
Total number of messages for which all retry attempts were exhausted without success. |
Timer
| Micrometer name | Prometheus name | Tags | Description |
|---|---|---|---|
hermes-client.latency |
hermes_client_latency_seconds_count / hermes_client_latency_seconds_sum / hermes_client_latency_seconds_max |
topic |
Time spent sending a message to Hermes, recorded for every publish attempt including retries. |
Distribution summary (histogram)
| Micrometer name | Prometheus name | Tags | Description |
|---|---|---|---|
hermes-client.retries.attempts |
hermes_client_retries_attempts_count / hermes_client_retries_attempts_sum / hermes_client_retries_attempts_max |
topic |
Distribution of the number of retry attempts per message. Recorded when the publish completes without exhausting all retries — value is 0 when no retries were needed. Not recorded when all retries are exhausted. |
Example dashboard queries
Below are example PromQL / MetricsQL queries that can be used to build a Hermes topic monitoring dashboard.
Publish success rate (%):
sum(rate(hermes_client_publish_finally_success_total{topic="com_group.topic"}[5m]))
/
sum(rate(hermes_client_publish_attempt_total{topic="com_group.topic"}[5m]))
* 100
Publish latency (p99):
Note: histogram_quantile requires histogram buckets to be enabled in Micrometer configuration
(e.g. via publishPercentileHistogram(true) on the Timer). Without that, use hermes_client_latency_seconds_max
for an approximation, or configure percentiles on the client side.
histogram_quantile(0.99, rate(hermes_client_latency_seconds_bucket{topic="com_group.topic"}[5m]))
Retry rate (% of publishes that required retries):
sum(rate(hermes_client_publish_retry_attempt_total{topic="com_group.topic"}[5m]))
/
sum(rate(hermes_client_publish_attempt_total{topic="com_group.topic"}[5m]))
* 100
Average number of retries per message:
sum(rate(hermes_client_retries_attempts_sum{topic="com_group.topic"}[5m]))
/
sum(rate(hermes_client_retries_attempts_count{topic="com_group.topic"}[5m]))
Retries exhausted rate:
sum(rate(hermes_client_retries_exhausted_total{topic="com_group.topic"}[5m]))
Sender implementations
Spring - WebClient
Requirement: org.springframework:spring-webflux must be provided at runtime.
HermesClient client = HermesClientBuilder.hermesClient(new WebClientHermesSender(WebClient.create()))
.withURI(URI.create("http://localhost:8080"))
.build();
Spring - AsyncRestTemplate
Requirement: org.springframework:spring-web must be provided at runtime.
HermesClient client = HermesClientBuilder.hermesClient(new RestTemplateHermesSender(new AsyncRestTemplate()))
.withURI(URI.create("http://localhost:8080"))
.build();
Jersey Client
Requirement: org.glassfish.jersey.core:jersey-client must be provided at runtime.
HermesClient client = HermesClientBuilder.hermesClient(new JerseyHermesSender(ClientBuilder.newClient()))
.withURI(URI.create("http://localhost:8080"))
.build();
OkHttp Client
Requirement: com.squareup.okhttp3:okhttp must be provided at runtime.
HermesClient client = HermesClientBuilder.hermesClient(new OkHttpHermesSender(new OkHttpClient()))
.withURI(URI.create("http://localhost:8080"))
.build();
HTTP2 support
Requirements:
JVM configured with ALPN support:
java -Xbootclasspath/p:<path_to_alpn_boot_jar> ...
OkHttp Client configured with SSL support:
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory())
.build();
HermesClient client = HermesClientBuilder.hermesClient(new OkHttpHermesSender(okHttpClient))
.withURI(URI.create("https://localhost:8443"))
.build();
Custom HermesSender
Example with Unirest - very simple http client.
HermesClient client = HermesClientBuilder.hermesClient((uri, message) -> {
CompletableFuture<HermesResponse> future = new CompletableFuture<>();
Unirest.post(uri.toString()).body(message.getBody()).asStringAsync(new Callback<String>() {
@Override
public void completed(HttpResponse<String> response) {
future.complete(() -> response.getStatus());
}
@Override
public void failed(UnirestException exception) {
future.completeExceptionally(exception);
}
@Override
public void cancelled() {
future.cancel(true);
}
});
return future;
})
.withURI(URI.create("http://localhost:8080"))
.build();