1616
1717package io .helidon .webclient .telemetry .metrics ;
1818
19- import java .util .ArrayList ;
2019import java .util .List ;
21- import java .util .concurrent .TimeUnit ;
20+ import java .util .logging .Logger ;
21+ import java .util .regex .Pattern ;
2222
2323import io .helidon .common .media .type .MediaTypes ;
24- import io .helidon .http .Method ;
25- import io .helidon .metrics .api .MeterRegistry ;
26- import io .helidon .metrics .api .Timer ;
2724import io .helidon .webclient .api .WebClient ;
2825import io .helidon .webserver .WebServer ;
2926import io .helidon .webserver .http .HttpRouting ;
3027import io .helidon .webserver .testing .junit5 .ServerTest ;
3128import io .helidon .webserver .testing .junit5 .SetUpRoute ;
3229
30+ import io .opentelemetry .exporter .logging .otlp .OtlpJsonLoggingMetricExporter ;
31+ import org .hamcrest .FeatureMatcher ;
32+ import org .hamcrest .Matcher ;
3333import org .junit .jupiter .api .Test ;
3434
3535import static org .hamcrest .MatcherAssert .assertThat ;
3636import static org .hamcrest .Matchers .allOf ;
37+ import static org .hamcrest .Matchers .containsString ;
3738import static org .hamcrest .Matchers .equalTo ;
38- import static org .hamcrest .Matchers .greaterThan ;
39- import static org .hamcrest .Matchers .hasEntry ;
40- import static org .hamcrest .Matchers .hasSize ;
4139import static org .hamcrest .Matchers .is ;
40+ import static org .hamcrest .Matchers .not ;
4241
4342@ ServerTest
4443class TestClientMetrics {
4544
4645 private final WebServer server ;
47- private final MeterRegistry meterRegistry ;
4846
49- TestClientMetrics (WebServer server , MeterRegistry meterRegistry ) {
47+ TestClientMetrics (WebServer server ) {
5048 this .server = server ;
51- this .meterRegistry = meterRegistry ;
5249 }
5350
5451 @ SetUpRoute
@@ -57,67 +54,69 @@ static void setup(HttpRouting.Builder builder) {
5754 }
5855
5956 @ Test
60- void testClientMetrics () {
61- var client = WebClient .builder ()
62- .baseUri ("http://localhost:" + server .port ())
63- .addService (WebClientTelemetryMetrics .create ())
64- .build ();
65-
66- var response = client .get ("/greet" )
67- .accept (MediaTypes .TEXT_PLAIN )
68- .request (String .class );
69-
70- assertThat ("Response status" , response .status ().code (), is (200 ));
71-
72- List <Timer > timers = meterRegistry .meters (meter -> meter .id ().name ().equals (WebClientTelemetryMetrics .REQUEST_DURATION ))
73- .stream ()
74- .filter (m -> m instanceof Timer )
75- .map (m -> (Timer ) m )
76- .toList ();
77-
78- assertThat ("Timers" , timers , hasSize (equalTo (1 )));
79-
80- Timer goodTimer = timers .getFirst ();
81-
82- assertThat ("Timer count" , goodTimer .count (), is (greaterThan (0L )));
83- assertThat ("Timer duration" , goodTimer .totalTime (TimeUnit .NANOSECONDS ), is (greaterThan (0D )));
84- var tags = goodTimer .id ().tagsMap ();
85-
86- assertThat ("Timer tags" , tags , allOf (
87- hasEntry (WebClientTelemetryMetrics .HTTP_REQUEST_METHOD , Method .GET_NAME ),
88- hasEntry (WebClientTelemetryMetrics .SERVER_ADDRESS , "localhost" ),
89- hasEntry (WebClientTelemetryMetrics .SERVER_PORT , Integer .toString (server .port ())),
90- hasEntry (WebClientTelemetryMetrics .ERROR_TYPE , "" ),
91- hasEntry (WebClientTelemetryMetrics .HTTP_RESPONSE_STATUS_CODE , "200" ),
92- hasEntry (WebClientTelemetryMetrics .URL_SCHEME , "http" )));
93-
94- var response404 = client .get ("/missing" )
95- .accept (MediaTypes .TEXT_PLAIN )
96- .request (String .class );
97-
98- assertThat ("Expected failed response" , response404 .status ().code (), is (404 ));
99-
100- timers = new ArrayList <>(meterRegistry .meters (meter -> meter .id ().name ().equals (WebClientTelemetryMetrics .REQUEST_DURATION ))
101- .stream ()
102- .filter (m -> m instanceof Timer )
103- .map (m -> (Timer ) m )
104- .toList ());
105- timers .remove (goodTimer );
106-
107- assertThat ("Bad timer count" , timers , hasSize (1 ));
108-
109- var badTimer = timers .getFirst ();
110- assertThat ("Timer count" , badTimer .count (), is (greaterThan (0L )));
111- assertThat ("Bad timer duration" , badTimer .totalTime (TimeUnit .NANOSECONDS ), is (greaterThan (0D )));
112-
113- tags = badTimer .id ().tagsMap ();
114- assertThat ("Bad timer tags" , tags , allOf (
115- hasEntry (WebClientTelemetryMetrics .HTTP_REQUEST_METHOD , Method .GET_NAME ),
116- hasEntry (WebClientTelemetryMetrics .SERVER_ADDRESS , "localhost" ),
117- hasEntry (WebClientTelemetryMetrics .SERVER_PORT , Integer .toString (server .port ())),
118- hasEntry (WebClientTelemetryMetrics .ERROR_TYPE , "404" ),
119- hasEntry (WebClientTelemetryMetrics .HTTP_RESPONSE_STATUS_CODE , "404" ),
120- hasEntry (WebClientTelemetryMetrics .URL_SCHEME , "http" )));
57+ void testClientMetrics () throws InterruptedException {
58+ try (TestLogHandler handler = TestLogHandler .create (Logger .getLogger (OtlpJsonLoggingMetricExporter .class .getName ()))) {
12159
60+ var client = WebClient .builder ()
61+ .baseUri ("http://localhost:" + server .port ())
62+ .addService (WebClientTelemetryMetrics .create ())
63+ .build ();
64+
65+ var response = client .get ("/greet" )
66+ .accept (MediaTypes .TEXT_PLAIN )
67+ .request (String .class );
68+
69+ assertThat ("Response status" , response .status ().code (), is (200 ));
70+
71+ List <String > messages = handler .messages (1 );
72+
73+ var patternText = ".*\" dataPoints\" :.*\" count\" :\" ([^\" ]+)\" .*\" sum\" :([^,]+),(.*)" ;
74+
75+ var jsonPattern = Pattern .compile (patternText );
76+ var matcher = jsonPattern .matcher (messages .getFirst ());
77+
78+ assertThat ("Matched log line" , matcher , allOf (
79+ matches (true ),
80+ hasGroupAsInteger (1 , is (1 )),
81+ hasGroupAsDouble (2 , not (equalTo (0D ))),
82+ hasGroupAsString (3 , containsString ("\" key\" :\" " + WebClientTelemetryMetrics .URL_TEMPLATE + "\" ,"
83+ + "\" value\" :{\" stringValue\" :\" /greet\" " ))));
84+
85+ }
86+ }
87+ static Matcher <java .util .regex .Matcher > matches (boolean expected ) {
88+ return new FeatureMatcher <>(is (expected ), "matches text" , "matches" ) {
89+ @ Override
90+ protected Boolean featureValueOf (java .util .regex .Matcher actual ) {
91+ return actual .matches ();
92+ }
93+ };
94+ }
95+
96+ static Matcher <java .util .regex .Matcher > hasGroupAsString (int groupNumber , Matcher <String > matcher ) {
97+ return new FeatureMatcher <>(matcher , "matches group " + groupNumber , "matches" ) {
98+ @ Override
99+ protected String featureValueOf (java .util .regex .Matcher actual ) {
100+ return actual .group (groupNumber );
101+ }
102+ };
103+ }
104+
105+ static Matcher <java .util .regex .Matcher > hasGroupAsInteger (int groupNumber , Matcher <Integer > matcher ) {
106+ return new FeatureMatcher <>(matcher , "matches group " + groupNumber , "matches" ) {
107+ @ Override
108+ protected Integer featureValueOf (java .util .regex .Matcher actual ) {
109+ return Integer .parseInt (actual .group (groupNumber ));
110+ }
111+ };
112+ }
113+
114+ static Matcher <java .util .regex .Matcher > hasGroupAsDouble (int groupNumber , Matcher <Double > matcher ) {
115+ return new FeatureMatcher <>(matcher , "matches group " + groupNumber , "matches" ) {
116+ @ Override
117+ protected Double featureValueOf (java .util .regex .Matcher actual ) {
118+ return Double .parseDouble (actual .group (groupNumber ));
119+ }
120+ };
122121 }
123122}
0 commit comments