As of October 1, 2023, LINE has been rebranded as LY Corporation. Visit the new blog of LY Corporation here: LY Corporation Tech Blog

Blog


Customizing Armeria metrics

In my last post, Monitoring Prometheus metrics from Armeria, we took a look at how you can monitor Armeria metrics using Grafana. In this post, I would like to show you how you can customize Armeria metrics to suit your needs.

The sample code in this post uses the same sample code from Monitoring Prometheus metrics from Armeria.

Customizing metrics prefixes using MeterIdPrefixFunction

In the last post I mentioned that you can customize the default prefix appended to metrics by using the MeterIdPrefixFunction#ofDefault function. You can further customize these prefixes by using the MeterIdPrefixFunction#andThen function. 

Let's assume you want to add an HTTP method name as your prefix.

  • AS-IS: my_http_service
  • TO-BE: my_http_service_{HTTP method}

First you must generate a class that implements MeterIdPrefixFunctionCustomizer. You can add your HTTP method name to your existing MeterIdPrefix in the  MeterIdPrefixFunctionCustomizer#apply function.

While doing so, you can use RequestLog#requestHeaders to get the request's HTTP method name. Various other information generated while processing a single request gets collected on RequestLog. For more information about this, please refer to the Official Armeria documentation.

MeterIdPrefixFunctionCustomizer.java

public class MyMeterIdPrefixFunction implements MeterIdPrefixFunctionCustomizer {
 
    @Override
    public MeterIdPrefix apply(MeterRegistry registry, RequestOnlyLog log, MeterIdPrefix meterIdPrefix) {
        return meterIdPrefix.append(log.requestHeaders().method().name());
    }
}

Next we insert the class's instance using the MeterIdPrefixFunction#andThen function.

ArmeriaPrometheusApplication.java

ServerBuilder sb = Server.builder();
                         .http(8083)
                         .meterRegistry(meterRegistry);
sb.annotatedService(...);
sb.service("/metrics", ...);
sb.decorator(MetricCollectingService.builder(MeterIdPrefixFunction.ofDefault("my.http.service")
                                                                  // add
                                                                  .andThen(new MyMeterIdPrefixFunction()))
                                                                  .newDecorator());
Server server = sb.build();
// ...

After restarting the server, you can see that the HTTP method name has been appended to the front of your metrics.

$ curl -s http://localhost:8083/metrics | grep "my_http_service_"
# HELP my_http_service_GET_timeouts_total
# TYPE my_http_service_GET_timeouts_total counter
my_http_service_GET_timeouts_total{cause="RequestTimeoutException",hostname_pattern="*",http_status="500",method="hello",service="com.example.armeria_prometheus.MyAnnotatedService",} 0.0
my_http_service_GET_timeouts_total{cause="RequestTimeoutException",hostname_pattern="*",http_status="200",method="hello",service="com.example.armeria_prometheus.MyAnnotatedService",} 0.0
# HELP my_http_service_GET_active_requests
# TYPE my_http_service_GET_active_requests gauge
my_http_service_GET_active_requests{hostname_pattern="*",method="hello",service="com.example.armeria_prometheus.MyAnnotatedService",} 0.0
...

For an alternative method of doing this, you can use lambda expressions that have been introduced in Java 8.

ArmeriaPrometheusApplication.java

MeterIdPrefixFunction.ofDefault("my.http.service")
                     .andThen((registry, log, prefix) -> prefix.append(log.requestHeaders().method().name())))

Customizing HTTP API response success predicate

Armeria's default settings are set to recognize HTTP API response status codes smaller than 100 or larger than 400 as failures. However, you can use MetricCollectingServiceBuilder#successFunction to customize this predicate. 

Let's assume the API has received a 404 status code response.

MyAnnotatedService.java

public class MyAnnotatedService {
 
    @Get("/hello/{seq}")
    public HttpResponse hello(@Param("seq") int seq) {
        if (seq % 5 == 0) {
            return HttpResponse.of(HttpStatus.NOT_FOUND);
        }
        // ...
    }
}
$ curl http://localhost:8083/hello/5
404 Not Found
 
$ curl -s http://localhost:8083/metrics | grep "my_http_service_GET_requests_total" | grep "404"
my_http_service_GET_requests_total{hostname_pattern="*",http_status="404",method="hello",result="success",service="com.example.armeria_prometheus.MyAnnotatedService",} 0.0
my_http_service_GET_requests_total{hostname_pattern="*",http_status="404",method="hello",result="failure",service="com.example.armeria_prometheus.MyAnnotatedService",} 1.0

If you wish to change a 404 status code from a failure to a success, get the HTTP status from RequestLog#responseHeaders and implement MetricCollectingServiceBuilder#successFunction as follows.

ArmeriaPrometheusApplication.java

// ...               
sb.decorator(MetricCollectingService.builder(MeterIdPrefixFunction.ofDefault("my.http.service")
                                                                  .andThen(new MyMeterIdPrefixFunction()))
                                     // add
                                     .successFunction((context, log) -> {
                                        final int statusCode = log.responseHeaders().status().code();
                                        return (statusCode >= 200 && statusCode < 400) || statusCode == 404;
                                     })
                                     .newDecorator());
// ...

You can see that the test results below show the 404 status code response as a success.

$ curl -s http://localhost:8083/metrics | grep "my_http_service_GET_requests_total" | grep "404"
my_http_service_GET_requests_total{hostname_pattern="*",http_status="404",method="hello",result="success",service="com.example.armeria_prometheus.MyAnnotatedService",} 1.0
my_http_service_GET_requests_total{hostname_pattern="*",http_status="404",method="hello",result="failure",service="com.example.armeria_prometheus.MyAnnotatedService",} 0.0

Filtering metrics

You can filter out unwanted metrics from your logs by using MeterFilter. For example, if you wish to remove JVM metrics from your logs, configure MeterFilter as follows.

ArmeriaPrometheusApplication.java

public static void main(String[] args) {
        PrometheusMeterRegistry meterRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
        meterRegistry.config()
                     .meterFilter(MeterFilter.denyNameStartsWith("jvm"));
        ServerBuilder sb = Server.builder();
                                 .http(8083)
                                 .meterRegistry(meterRegistry);
        // ... 

You can see that all jvm_ metrics have been removed.

$ curl -s http://localhost:8083/metrics | grep "^jvm"

Collecting gRPC metrics

Using Armeria's GrpcMeterIdPrefixFunction, you can collect metrics related to gRPC status codes. To do this, add armeria-grpc to your dependencies as follows.

build.gradle

dependencies {
  
    // Armeria
    implementation "com.linecorp.armeria:armeria:1.8.0"
    implementation "com.linecorp.armeria:armeria-logback:1.8.0"
    implementation "com.linecorp.armeria:armeria-grpc:1.8.0" // add
  
    // ...
}

For testing purposes, I've created a simple gRPC service as follows.

hello.proto

syntax = "proto3";

package com.example.armeria_prometheus;

option java_package = "com.example.armeria_prometheus.grpc";

service HelloService {
  rpc Hello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  int32 seq = 1;
}

message HelloReply {
  string message = 1;
}

MyGrpcService.java

public class MyGrpcService extends HelloServiceGrpc.HelloServiceImplBase {
 
    @Override
    public void hello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
        if (request.getSeq() % 3 != 0) {
            HelloReply reply = HelloReply.newBuilder()
                                         .setMessage("Success")
                                         .build();
            responseObserver.onNext(reply);
            responseObserver.onCompleted();
            return;
        }
        responseObserver.onError(Status.INTERNAL.asException());
    }
}

I've added the gRPC service created above as a new service. In order to prevent affecting existing HTTP metric collection, I've applied a MetricCollectingService decorator to each service.

ArmeriaPrometheusApplication.java

// ... 
sb.annotatedService(new MyAnnotatedService(),
                    MetricCollectingService.builder(MeterIdPrefixFunction.ofDefault("my.http.service")
                                                                          // ... 생략 ...
sb.service(GrpcService.builder()
                      .addService(new MyGrpcService())
                      .build(),
           MetricCollectingService.newDecorator(GrpcMeterIdPrefixFunction.of("my.grpc.service")));
sb.service("/metrics", PrometheusExpositionService.of(meterRegistry.getPrometheusRegistry()));
// ... 

For easier testing, I've added client code that triggers multiple gRPC requests. Writing client code is also made easier when you're using Armeria.

RpcClientApplication.java

public class RpcClientApplication {

    private static final Logger logger = LoggerFactory.getLogger(RpcClientApplication.class);

    public static void main(String[] args) {
        HelloServiceBlockingStub helloService = Clients
                .newClient("gproto+http://127.0.0.1:8083/", HelloServiceBlockingStub.class);
        for (int i = 0; i < 100; i++) {
            try {
                HelloRequest request = HelloRequest.newBuilder().setSeq(i).build();
                HelloReply reply = helloService.hello(request);
                logger.info(reply.getMessage());
            } catch (Exception e) {
                logger.error("Error", e);
            }
        }
    }
}

Restart your server and run the client code.

$ curl -s http://localhost:8083/metrics | grep "my_grpc_service_requests_total"
# HELP my_grpc_service_requests_total
# TYPE my_grpc_service_requests_total counter
my_grpc_service_requests_total{grpc_status="13",hostname_pattern="*",http_status="200",method="Hello",result="success",service="com.example.armeria_prometheus.HelloService",} 0.0
my_grpc_service_requests_total{grpc_status="13",hostname_pattern="*",http_status="200",method="Hello",result="failure",service="com.example.armeria_prometheus.HelloService",} 34.0
my_grpc_service_requests_total{grpc_status="0",hostname_pattern="*",http_status="200",method="Hello",result="failure",service="com.example.armeria_prometheus.HelloService",} 0.0
my_grpc_service_requests_total{grpc_status="0",hostname_pattern="*",http_status="200",method="Hello",result="success",service="com.example.armeria_prometheus.HelloService",} 66.0

You can see that the gRPC status code metrics are collected and displayed.

Conclusion

Armeria also provides features that various users can use for collecting metrics. I hope many server engineers find these features useful and make their monitoring efforts more productive.