Record server metrics for cancelled connections

Prior to this commit, the Actuator instrumentation for WebFlux servers
would not record metrics in two cases:

* the client disconnects before the response has been sent
* a server timeout is triggered before the response is sent

This commit improves the existing instrumentation to record metrics in
these cases. Since the causes of timeouts/disconnections can vary a lot,
the chosen "outcome" tag for metrics is "UNKNOWN".

Closes gh-23606
This commit is contained in:
Brian Clozel
2021-03-29 21:27:50 +02:00
parent 9b65409e23
commit 61573fbf14
6 changed files with 120 additions and 25 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.boot.actuate.metrics.web.reactive.server;
import java.io.EOFException;
import java.time.Duration;
import io.micrometer.core.instrument.MockClock;
@@ -24,6 +25,7 @@ import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.actuate.metrics.AutoTimer;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@@ -116,6 +118,32 @@ class MetricsWebFilterTests {
assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("status", "200").timer().count()).isEqualTo(2);
}
@Test
void cancelledConnectionsShouldProduceMetrics() {
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
Mono<Void> processing = this.webFilter.filter(exchange,
(serverWebExchange) -> exchange.getResponse().setComplete());
StepVerifier.create(processing).thenCancel().verify(Duration.ofSeconds(5));
assertMetricsContainsTag("uri", "/projects/{project}");
assertMetricsContainsTag("status", "200");
assertMetricsContainsTag("outcome", "UNKNOWN");
}
@Test
void disconnectedExceptionShouldProduceMetrics() {
MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}");
Mono<Void> processing = this.webFilter
.filter(exchange, (serverWebExchange) -> Mono.error(new EOFException("Disconnected")))
.onErrorResume((t) -> {
exchange.getResponse().setRawStatusCode(500);
return exchange.getResponse().setComplete();
});
StepVerifier.create(processing).expectComplete().verify(Duration.ofSeconds(5));
assertMetricsContainsTag("uri", "/projects/{project}");
assertMetricsContainsTag("status", "500");
assertMetricsContainsTag("outcome", "UNKNOWN");
}
private MockServerWebExchange createExchange(String path, String pathPattern) {
PathPatternParser parser = new PathPatternParser();
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path).build());

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package org.springframework.boot.actuate.metrics.web.reactive.server;
import java.io.EOFException;
import io.micrometer.core.instrument.Tag;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -140,7 +142,7 @@ class WebFluxTagsTests {
@Test
void outcomeTagIsSuccessWhenResponseStatusIsNull() {
this.exchange.getResponse().setStatusCode(null);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("SUCCESS");
}
@@ -153,56 +155,68 @@ class WebFluxTagsTests {
given(response.getRawStatusCode()).willReturn(null);
given(exchange.getRequest()).willReturn(request);
given(exchange.getResponse()).willReturn(response);
Tag tag = WebFluxTags.outcome(exchange);
Tag tag = WebFluxTags.outcome(exchange, null);
assertThat(tag.getValue()).isEqualTo("SUCCESS");
}
@Test
void outcomeTagIsInformationalWhenResponseIs1xx() {
this.exchange.getResponse().setStatusCode(HttpStatus.CONTINUE);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("INFORMATIONAL");
}
@Test
void outcomeTagIsSuccessWhenResponseIs2xx() {
this.exchange.getResponse().setStatusCode(HttpStatus.OK);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("SUCCESS");
}
@Test
void outcomeTagIsRedirectionWhenResponseIs3xx() {
this.exchange.getResponse().setStatusCode(HttpStatus.MOVED_PERMANENTLY);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("REDIRECTION");
}
@Test
void outcomeTagIsClientErrorWhenResponseIs4xx() {
this.exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
}
@Test
void outcomeTagIsServerErrorWhenResponseIs5xx() {
this.exchange.getResponse().setStatusCode(HttpStatus.BAD_GATEWAY);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("SERVER_ERROR");
}
@Test
void outcomeTagIsClientErrorWhenResponseIsNonStandardInClientSeries() {
this.exchange.getResponse().setRawStatusCode(490);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("CLIENT_ERROR");
}
@Test
void outcomeTagIsUnknownWhenResponseStatusIsInUnknownSeries() {
this.exchange.getResponse().setRawStatusCode(701);
Tag tag = WebFluxTags.outcome(this.exchange);
Tag tag = WebFluxTags.outcome(this.exchange, null);
assertThat(tag.getValue()).isEqualTo("UNKNOWN");
}
@Test
void outcomeTagIsClientErrorWhenExceptionIsDisconnectedClient() {
Tag tag = WebFluxTags.outcome(this.exchange, new EOFException("broken pipe"));
assertThat(tag.getValue()).isEqualTo("UNKNOWN");
}
@Test
void outcomeTagIsClientErrorWhenExceptionIsCancelledExchange() {
Tag tag = WebFluxTags.outcome(this.exchange, new CancelledServerWebExchangeException());
assertThat(tag.getValue()).isEqualTo("UNKNOWN");
}