Skip to content

Commit

Permalink
EPA-105: Expose metrics on a separate port (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
michelealbanese-oviva authored May 7, 2024
1 parent 20095c5 commit dc1c4eb
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class ConfigReader {
public static final String CONFIG_BASE_URI = "base_uri";
public static final String CONFIG_HOST = "host";
public static final String CONFIG_PORT = "port";
public static final String CONFIG_MANAGEMENT_PORT = "management_port";
public static final String CONFIG_REDIRECT_URIS = "redirect_uris";

public static final String CONFIG_IDP_DISCOVERY_URI = "idp_discovery_uri";
Expand Down Expand Up @@ -60,7 +61,8 @@ public Config read() {
"no '%s' configured".formatted(CONFIG_IDP_DISCOVERY_URI)));

var host = configProvider.get(CONFIG_HOST).orElse("0.0.0.0");
var port = getPortConfig();
var port = getPortConfig(CONFIG_PORT, 1234);
var managementPort = getPortConfig(CONFIG_MANAGEMENT_PORT, 1235);

var fedmaster =
configProvider
Expand Down Expand Up @@ -104,6 +106,7 @@ public Config read() {
federationConfig,
host,
port,
managementPort,
baseUri,
idpDiscoveryUri,
sessionStoreConfig(),
Expand Down Expand Up @@ -150,11 +153,11 @@ private List<String> getScopes() {
.toList();
}

private int getPortConfig() {
return configProvider.get(CONFIG_PORT).stream()
private int getPortConfig(String configPort, int defaultValue) {
return configProvider.get(configPort).stream()
.mapToInt(Integer::parseInt)
.findFirst()
.orElse(1234);
.orElse(defaultValue);
}

private JWKSet loadJwks(String configName) {
Expand All @@ -176,6 +179,7 @@ public record Config(
FederationConfig federation,
String host,
int port,
int managementPort,
URI baseUri,
URI idpDiscoveryUri,
SessionStoreConfig sessionStore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@
import com.oviva.ehealthid.relyingparty.svc.TokenIssuerImpl;
import com.oviva.ehealthid.relyingparty.util.DiscoveryJwkSetSource;
import com.oviva.ehealthid.relyingparty.ws.App;
import com.oviva.ehealthid.relyingparty.ws.HealthEndpoint;
import com.oviva.ehealthid.relyingparty.ws.MetricsEndpoint;
import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics;
import io.micrometer.prometheus.PrometheusConfig;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.undertow.Handlers;
import io.undertow.Undertow;
import jakarta.ws.rs.SeBootstrap;
import jakarta.ws.rs.SeBootstrap.Configuration;
import jakarta.ws.rs.SeBootstrap.Instance;
import jakarta.ws.rs.core.UriBuilder;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.time.Clock;
Expand All @@ -59,7 +64,8 @@ public class Main implements AutoCloseable {
private static final String CONFIG_PREFIX = "EHEALTHID_RP";
private final ConfigProvider configProvider;

private Instance server;
private SeBootstrap.Instance server;
private Undertow managementServer;

private CountDownLatch shutdown = new CountDownLatch(1);

Expand Down Expand Up @@ -90,6 +96,13 @@ public URI baseUri() {
return server.configuration().baseUri();
}

public URI managementBaseUri() {
var baseUri = server.configuration().baseUri();
var address = (InetSocketAddress) managementServer.getListenerInfo().get(0).getAddress();

return UriBuilder.fromUri(baseUri).port(address.getPort()).build();
}

public void start() throws ExecutionException, InterruptedException {

logger.atInfo().log("\n" + BANNER);
Expand Down Expand Up @@ -136,20 +149,31 @@ public void start() throws ExecutionException, InterruptedException {

server =
SeBootstrap.start(
new App(
config, keyStore, tokenIssuer, clientAuthenticator, meterRegistry, authService),
new App(config, keyStore, tokenIssuer, clientAuthenticator, authService),
Configuration.builder().host(config.host()).port(config.port()).build())
.toCompletableFuture()
.get();

var localUri = server.configuration().baseUri();
logger.atInfo().log("Magic at {} ({})", config.baseUri(), localUri);

managementServer =
Undertow.builder()
.addHttpListener(config.managementPort(), config.host())
.setHandler(
Handlers.path()
.addExactPath(HealthEndpoint.PATH, new HealthEndpoint())
.addExactPath(MetricsEndpoint.PATH, new MetricsEndpoint(meterRegistry)))
.build();
managementServer.start();

logger.atInfo().log("Management Server can be found at port {}", config.managementPort());
}

private AuthenticationFlow buildAuthFlow(
URI selfIssuer, URI fedmaster, JWKSet encJwks, HttpClient httpClient) {

// setup the file `.env.properties` to provide the X-Authorization header for the Gematik
// set up the file `.env.properties` to provide the X-Authorization header for the Gematik
// test environment
// see: https://wiki.gematik.de/display/IDPKB/Fachdienste+Test-Umgebungen
var fedHttpClient = new GematikHeaderDecoratorHttpClient(new JavaHttpClient(httpClient));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import com.oviva.ehealthid.relyingparty.svc.KeyStore;
import com.oviva.ehealthid.relyingparty.svc.TokenIssuer;
import com.oviva.ehealthid.util.JoseModule;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import jakarta.ws.rs.core.Application;
import java.util.Set;

Expand All @@ -20,7 +19,6 @@ public class App extends Application {
private final KeyStore keyStore;
private final TokenIssuer tokenIssuer;
private final ClientAuthenticator clientAuthenticator;
private final PrometheusMeterRegistry prometheusMeterRegistry;

private final AuthService authService;

Expand All @@ -29,13 +27,11 @@ public App(
KeyStore keyStore,
TokenIssuer tokenIssuer,
ClientAuthenticator clientAuthenticator,
PrometheusMeterRegistry prometheusMeterRegistry,
AuthService authService) {
this.config = config;
this.keyStore = keyStore;
this.tokenIssuer = tokenIssuer;
this.clientAuthenticator = clientAuthenticator;
this.prometheusMeterRegistry = prometheusMeterRegistry;
this.authService = authService;
}

Expand All @@ -47,9 +43,7 @@ public Set<Object> getSingletons() {
new AuthEndpoint(authService),
new TokenEndpoint(tokenIssuer, clientAuthenticator),
new OpenIdEndpoint(config.baseUri(), config.relyingParty(), keyStore),
new JacksonJsonProvider(configureObjectMapper()),
new HealthEndpoint(),
new MetricsEndpoint(prometheusMeterRegistry));
new JacksonJsonProvider(configureObjectMapper()));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package com.oviva.ehealthid.relyingparty.ws;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;

@Path("/health")
public class HealthEndpoint {
public class HealthEndpoint implements HttpHandler {
public static final String PATH = "/health";

private static final int HTTP_METHOD_NOT_ALLOWED = 405;
private static final int HTTP_OK = 200;

private static final String STATUS_UP = "{\"status\":\"UP\"}";

@GET
public Response get() {
// For now if this endpoint is reachable then the service is up. There is no hard dependency
// that could be down.
return Response.ok(STATUS_UP).type(MediaType.APPLICATION_JSON_TYPE).build();
@Override
public void handleRequest(HttpServerExchange httpServerExchange) {
if (!httpServerExchange.getRequestMethod().equals(HttpString.tryFromString("GET"))) {
httpServerExchange.setStatusCode(HTTP_METHOD_NOT_ALLOWED);
httpServerExchange.getResponseSender().send("");
} else {
// For now if this endpoint is reachable then the service is up.
// There is no hard dependency that could be down.
httpServerExchange.setStatusCode(HTTP_OK);
httpServerExchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json");
httpServerExchange.getResponseSender().send(STATUS_UP);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import io.micrometer.core.instrument.binder.jvm.JvmThreadMetrics;
import io.micrometer.core.instrument.binder.system.ProcessorMetrics;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;

@Path("/metrics")
public class MetricsEndpoint {
public class MetricsEndpoint implements HttpHandler {
public static final String PATH = "/metrics";
private static final int HTTP_OK = 200;
private static final int HTTP_METHOD_NOT_ALLOWED = 405;

private final PrometheusMeterRegistry registry;

Expand All @@ -26,9 +28,17 @@ public MetricsEndpoint(PrometheusMeterRegistry registry) {
new JvmThreadMetrics().bindTo(this.registry);
}

@GET
@Produces(MediaType.TEXT_PLAIN)
public String get() {
return registry.scrape();
@Override
public void handleRequest(HttpServerExchange httpServerExchange) {
if (!httpServerExchange.getRequestMethod().equals(HttpString.tryFromString("GET"))) {
httpServerExchange.setStatusCode(HTTP_METHOD_NOT_ALLOWED);
httpServerExchange.getResponseSender().send("");
} else {
httpServerExchange.setStatusCode(HTTP_OK);
httpServerExchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");

var metricsContents = registry.scrape();
httpServerExchange.getResponseSender().send(metricsContents);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,28 @@ static void afterAll() throws Exception {

@Test
void run_smokeTest() {

var baseUri = application.baseUri();
var managementBaseUri = application.managementBaseUri();

// then
assertGetOk(baseUri.resolve(DISCOVERY_PATH));
assertGetOk(baseUri.resolve(JWKS_PATH));
assertGetOk(baseUri.resolve(FEDERATION_CONFIG_PATH));
assertGetOk(baseUri.resolve(HEALTH_PATH));
assertGetOk(baseUri.resolve(METRICS_PATH));

assertGetOk(managementBaseUri.resolve(HEALTH_PATH));
assertGetOk(managementBaseUri.resolve(METRICS_PATH));
}

@Test
void run_metrics() {

var baseUri = application.baseUri();
var managementBaseUri = application.managementBaseUri();

// when & then
get(baseUri.resolve(METRICS_PATH))
get(baseUri.resolve(METRICS_PATH)).then().statusCode(404);

get(managementBaseUri.resolve(METRICS_PATH))
.then()
.contentType(ContentType.TEXT)
.body(containsString("cache_gets_total{cache=\"sessionCache\""))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public URI start() throws ExecutionException, InterruptedException {
redirect_uris=%s
app_name=Awesome DiGA
port=0
management_port=0
"""
.formatted(wireMockServer.baseUrl(), discoveryUri, redirectUri));

Expand All @@ -53,6 +54,10 @@ public URI baseUri() {
return application.baseUri();
}

public URI managementBaseUri() {
return application.managementBaseUri();
}

public Stubbing wireMockStubbing() {
return wireMockServer;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package com.oviva.ehealthid.relyingparty.ws;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import io.undertow.io.Sender;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HeaderMap;
import io.undertow.util.HttpString;
import jakarta.ws.rs.core.Response.Status;
import org.junit.jupiter.api.Test;

Expand All @@ -12,9 +18,34 @@ void get() {
var sut = new HealthEndpoint();

// when
var res = sut.get();
var httpServerExchange = mock(HttpServerExchange.class);
var headers = mock(HeaderMap.class);
var sender = mock(Sender.class);

when(httpServerExchange.getResponseHeaders()).thenReturn(headers);
when(httpServerExchange.getResponseSender()).thenReturn(sender);
when(httpServerExchange.getRequestMethod()).thenReturn(HttpString.tryFromString("GET"));

sut.handleRequest(httpServerExchange);

// then
verify(httpServerExchange).setStatusCode(Status.OK.getStatusCode());
}

@Test
void methodNotAllowed() {
var sut = new HealthEndpoint();

// when
var httpServerExchange = mock(HttpServerExchange.class);
var sender = mock(Sender.class);

when(httpServerExchange.getResponseSender()).thenReturn(sender);
when(httpServerExchange.getRequestMethod()).thenReturn(HttpString.tryFromString("POST"));

sut.handleRequest(httpServerExchange);

// then
assertEquals(Status.OK.getStatusCode(), res.getStatus());
verify(httpServerExchange).setStatusCode(Status.METHOD_NOT_ALLOWED.getStatusCode());
}
}
Loading

0 comments on commit dc1c4eb

Please sign in to comment.