1.15.0 release notes
24th March 2022
🌸 Highlights
- gRPC health check protocol
- Multipart file uploads in annotated services
- Preliminary GraphQL support in
DocService - Refactored DNS resolver cache with higher hit ratio and reduced traffic
- New metrics that help you get alerts before your TLS certificates are expired
🌟 New features
Servernow registers two gauges about TLS certificate expiry, so you can get alerts before the certificates are expired. #4075 #4082armeria_server_certificate_validity1if certificate is in validity period.0otherwise.
armeria_server_certificate_validity_days- The number of days before certificate expires, or
-1if expired.
- The number of days before certificate expires, or
You can now specify a
FileorPathparameter in your annotated service method to handle multipart file uploads. #3578 #4061Server .builder() .annotatedService(new Object() { @Post("/upload") @Consumes("multipart/form-data") public String upload(@Param File file) { return "Successfully uploaded a file to " + file; } }) .build();You can now use the
||(logical OR) operator in parameter and header predicate expressions. #4116 #4138// "Hi!" for /greet?casual or /greet?casual=true // "Hello!" for /greet or /greet?casual=false Server .builder() .route() .get("/greet") .matchesParam("casual || casual=true") // 👈👈👈 .build((ctx, req) -> HttpResponse.of("Hi!")) .route() .get("/greet") .matchesParam("!casual || casual!=true") // 👈👈👈 .build(ctx, req) -> HttpResponse.of("Hello!"))You can now enable gRPC health check protocol with
GrpcHealthCheckService#3146 #3963Server .builder() .service( GrpcService .builder() .addService(new MyGrpcService()) .enableHealthCheckService(true) // 👈👈👈 // or customize: // .addService(GrpcHealthCheckService.of(...)) .build() ) .build()DocServicenow supportsGraphqlServicewith CodeMirror auto-completion. #3706 #4023You can now configure the
EndpointGroupimplementations based onDynamicEndpointGroupto allow or disallow empty endpoints. This can be useful when you want to avoid the situation where the endpoint group becomes empty due to misconfiguration. #3952 #3958EndpointGroup endpointGroup = DnsAddressEndpointGroup .builder("example.com") .allowEmptyEndpoints(false) // 👈👈👈 .build();You can now configure the timeout of DNS sub-queries with
queryTimeoutMillisForEachAttempt(). Previously, you were only able to specify the timeout for the entire DNS resolution process, which can consist of more than one DNS query. #2940 #4133DnsAddressEndpointGroup .builder("armeria.dev") .queryTimeoutMillisForEachAttempt(1000) // 👈👈👈 .queryTimeoutMillis(5000) .build();Armeria now has the global shared DNS cache by default for higher hit ratio and less DNS traffic. You can also build and share a custom DNS cache across multiple resolvers. #2940 #4133
DnsCache dnsCache = DnsCache .builder() .ttl(60, 3600) .negativeTtl(600) .build(); EndpointGroup endpointGroup = DnsAddressEndpointGroup .builder("armeria.dev") .dnsCache(dnsCache) // 👈👈👈 .build(); ClientFactory factory = ClientFactory .builder() .domainNameResolverCustomizer(builder -> { builder.dnsCache(dnsCache); // 👈👈👈 }) .build(); WebClient client = WebClient .builder(SessionProtocol.HTTPS, endpointGroup) // 👈👈👈 .factory(factory) // 👈👈👈 .build();Failed search domain queries are now cached according to the negative TTL configuration. #2940 #4133
You can now specify a
SuccessFunctionwhen constructing a client or server, and let all decorators use it for determining whether a request was successful or not. #4101Server .builder() .successFunction((ctx, log) -> { // 👈👈👈 // Treat only '200 OK' and '204 No Content' as success. switch (log.responseStatus().code()) { case 200: case 204: return true; default: return false; } }) .decorator(LoggingService.newDecorator()) .decorator(MetricCollectingService.newDecorator()) ... WebClient .builder() .successFunction(...) // 👈👈👈 .decorator(LoggingClient.newDecorator()) .decorator(MetricCollectingClient.newDecorator()) .build();You can now specify different sampling ratio (or
Sampler) for successful and failed requests when usingLoggingClientandLoggingService#3666 #4101LoggingService .builder() // Log at 10% sampling ratio if successful. .successSamplingRate(0.1) // Log all failure. .failureSamplingRate(1.0) .newDecorator()You can now customize how
LoggingClientandLoggingServicedetermines log level more flexibly. #3972LoggingService .builder() // Log at INFO if under /important. DEBUG otherwise. .requestLogLevelMapper(log -> { if (log.requestHeaders().path().startsWith("/important/")) { return LogLevel.INFO; } else { return LogLevel.DEBUG; } }) // Log at INFO if 200 OK. .responseLogLevel(HttpStatus.OK, LogLevel.INFO) // Log at WARN if 5xx. .responseLogLevel(HttpStatusClass.SERVER_ERROR, LogLevel.WARN) .newDecorator()You can now specify a customizer that customizes a
ClientRequestContextwhen building a client. #4075 #4082WebClient client = WebClient .builder() .contextCustomizer(ctx -> { // 👈👈👈 String userId = MyUserContext.current().getId(); ctx.setAttr(USER_ID, userId); }) .build();For instance, you could manually propagate a Brave thread-local
TraceContextusing it:Tracing reactorTracing = ...; Tracing requestContextTracing = Tracing .newBuilder() .currentTraceContext(RequestContextCurrentTraceContext.ofDefault()) .build(); WebClient .builder() .contextCustomizer(TraceContextPropagation.inject(() -> { // Propagate Reactor's TraceContext to Armeria's TraceContext. return reactorTracing.currentTraceContext().get(); }) .decorator(BraveClient.newDecorator(requestContextTracing)) .build();You can now write the content of
StreamMessageinto a file usingStreamMessage.writeTo()#4048 #4130SplitHttpResponse res = WebClient .of() .get("https://example.com/large_file") .split(); StreamMessage<HttpData> body = res.body(); body.writeTo( // 👈👈👈 Function.identity(), Path.of("/tmp", "large_file") );You can now convert a
StreamMessageinto anInputStreamusingStreamMessage.toInputStream()#4059StreamMessage<HttpData> body = ...; BufferedReader in = new BufferedReader( new InputStreamReader( body.toInputStream(Function.identity()), // 👈👈👈 "UTF-8" ) ); for (;;) { String line = in.readLine(); if (line == null) break; System.out.println(line); }You can now use
StreamMessage.mapParallel()to modify a stream using an async operation with a configurable concurrency limit. #4031You can now decode a
StreamMessageinto another usingStreamMessage.decode()which was previously possible only forHttpMessage. #4147 #4148 #4152You can now give Armeria a performance optimization hint by specifying if your service is unary, request-streaming, response-streaming or bidi-streaming by implementing
HttpService.exchangeType()#3956class MyUnaryService implements HttpService { @Override public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) { return HttpResponse.from( req .aggregate() // Always aggregate .thenApply(aggregatedReq -> { HttpResponse.of( // Never streaming "You sent %d bytes.", aggregatedReq.content().length() ); }) ); } // Tells Armeria to assume non-streaming requests and responses. @Override public ExchangeType exchangeType() { return ExchangeType.UNARY; // 👈👈👈 } }
📈 Improvements
- Spring Boot
WebServerExceptionmessage is now more user-friendly. #4146
🛠️ Bug fixes
- A response with status code
408doesn't trigger aNullPointerExceptionanymore, even if its reason phrase is not exactlyRequest Timeout. #4165 - Failed search domain queries are now cached properly according to the negative TTL configuration. #2940 #4133
GrpcServicenow logsRequestOnlyLog.requestContent()for invalid requests. #4128DocServicedoesn't crash the whole Armeria service anymore when.protofiles have comments on service options or extensions. #4123 #4127GraphqlServicedoesn't throw aClassCastExceptionanymore whenqueryoroperationNameare not a string. #4104 #4168FileServicedoesn't choose a.brfile for generating decompressed content anymore when Brotli is not available. #4119- You no longer see an
HttpResponseExceptionorHttpStatusExceptionfrom built-in services. #4056 #4117- Thanks to this fix, you can now mutate a redirect response from
FileServicein your decorator usingHttpResponse.mapHeaders()
- Thanks to this fix, you can now mutate a redirect response from
RampingUpEndpointWeightSelectordoesn't raise aNoSuchElementExceptionanymore when endpoints are replaced completely. #3776 #4102
🏚️ Deprecations
samplingRateproperty of@LoggingDecoratorhas been deprecated in favor ofsuccessSamplingRateandfailureSamplingRate.- The
successFunction()builder methods that require aBiPredicatehas been deprecated in favor ofSuccessFunctionthat's specified when constructing a client or server. dnsServerAddressStreamProvider()has been deprecated in favor ofserverAddressStreamProvider()in the DNS-related builders.disableDnsQueryMetrics()has been deprecated in favor ofenableDnsQueryMetrics()in the DNS-related builders.
☢️ Breaking changes
- The experimental
HttpDecoderAPI has been replaced withStreamDecoder#4147 #4152
⛓ Dependencies
- Bucket4j 7.0.0 → 7.3.0
- Curator 5.2.0 → 5.2.1
- Dropwizard Metrics 4.2.7 → 4.2.9
- gRPC-Java 1.43.2 → 1.45.0
- Jackson 2.13.1 → 2.13.2
- java-jwt 3.18.3 → 3.19.0
- Jetty 9.4.44 → 9.4.45
- Graphql-Java 17.3 → 18.0
- Logback 1.2.10 → 1.2.11
- Micrometer 1.8.2 → 1.8.4
- Netty 4.1.73 → 4.1.75
- io_uring 0.0.11 → 0.0.13
- Prometheus 0.14.1 → 0.15.0
- Reactor 3.4.14 → 3.4.16
- Reactor Kotlin 1.1.5 → 1.1.6
- RxJava 3.1.3 → 3.1.4
- Sangria 2.1.6 → 3.0.0
- ScalaPB 0.11.8 → 0.11.10
- scala-collection-compat 2.6.0 → 2.7.0
- SLF4J 1.7.34 → 1.7.36
- Spring 5.3.15 → 5.3.17
- Spring Boot 2.6.3 → 2.6.5