Skip to content

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:

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

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();