From 6f6a3fa621f4c6a8c1d28725e66226945ae7f666 Mon Sep 17 00:00:00 2001 From: Anukalp Date: Thu, 29 Jan 2026 20:01:15 +0530 Subject: [PATCH 1/6] Merge observability health, metrics and http into http-server --- fusion-http-server/pom.xml | 6 + .../http/server/impl/health/Health.java | 115 +++++++++++++++++ .../http/server/impl/health/HealthCheck.java | 46 +++++++ .../server/impl/health/HealthRegistry.java | 48 +++++++ .../impl/http/MonitoringEndpointRegistry.java | 38 ++++++ .../server/impl/http/ObservabilityServer.java | 110 ++++++++++++++++ .../http/server/impl/metrics/Metrics.java | 48 +++++++ .../server/impl/metrics/MetricsRegistry.java | 118 ++++++++++++++++++ .../impl/metrics/OpenMetricsFormatter.java | 41 ++++++ .../fusion/observability/health/Health.java | 2 +- .../observability/health/HealthCheck.java | 2 +- .../observability/health/HealthRegistry.java | 2 +- .../http/MonitoringEndpointRegistry.java | 10 +- .../http/ObservabilityServer.java | 2 +- .../fusion/observability/metrics/Metrics.java | 2 +- .../metrics/MetricsRegistry.java | 2 +- .../metrics/OpenMetricsFormatter.java | 2 +- 17 files changed, 582 insertions(+), 12 deletions(-) create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthCheck.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthRegistry.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/MetricsRegistry.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/OpenMetricsFormatter.java diff --git a/fusion-http-server/pom.xml b/fusion-http-server/pom.xml index fe62cad9..dc03ff2c 100644 --- a/fusion-http-server/pom.xml +++ b/fusion-http-server/pom.xml @@ -28,6 +28,12 @@ Fusion :: Http Server + + ${project.groupId} + fusion-build-api + ${project.version} + provided + ${project.groupId} fusion-api diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java new file mode 100644 index 00000000..31a1ff16 --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.health; + +import io.yupiik.fusion.http.server.api.Request; +import io.yupiik.fusion.http.server.api.Response; +import io.yupiik.fusion.http.server.spi.Endpoint; +import io.yupiik.fusion.http.server.impl.health.HealthCheck; +import io.yupiik.fusion.http.server.impl.health.HealthRegistry; + +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; + +import static java.util.Optional.ofNullable; +import static java.util.concurrent.CompletableFuture.allOf; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; + +public class Health implements Endpoint { + private final HealthRegistry healthChecks; + + public Health(final HealthRegistry healthChecks) { + this.healthChecks = healthChecks; + } + + @Override + public boolean matches(final Request request) { + return "GET".equalsIgnoreCase(request.method()) && "/health".equalsIgnoreCase(request.path()); + } + + @Override + public CompletionStage handle(final Request request) { + final var query = request.query(); + final var filter = query != null && query.startsWith("type=") ? + query.substring("type=".length()) : null; + + var healthCheckStream = healthChecks.healthChecks().stream(); + if (filter != null) { + healthCheckStream = healthCheckStream.filter(c -> Objects.equals(c.type(), filter)); + } + final var checks = healthCheckStream + .collect(toMap(identity(), c -> c.check().toCompletableFuture())); + return allOf(checks.values().toArray(new CompletableFuture[0])) + .thenApply(success -> success(checks, request)) + .exceptionally(failed -> failure(checks)); + } + + private Response failure(final Map> checks) { + final var response = Response.of() + .status(503) + .header("content-type", "text/plain") + .body(checks.entrySet().stream() + .map(c -> { + try { + return toSuccessLine(c); + } catch (final CompletionException | CancellationException ce) { + return c.getKey().name() + ",KO,\"" + ofNullable(ce.getMessage()).map(this::escape).orElse("") + "\""; + } + }) + .collect(joining("\n"))) + .build(); + + // cancel if any is still pending + checks.values().stream() + .filter(it -> !it.isCompletedExceptionally()) + .forEach(it -> { + try { + it.cancel(true); + } catch (final RuntimeException re) { + // no-op + } + }); + return response; + } + + private Response success(final Map> checks, final Request request) { + final var hasFailure = checks.values().stream() + .anyMatch(it -> it.getNow(null).status() == HealthCheck.Status.KO); + if (!hasFailure) request.setAttribute("skip-access-log", true); + return Response.of() + .status(hasFailure ? 503 : 200) + .header("content-type", "text/plain") + .body(checks.entrySet().stream() + .map(this::toSuccessLine) + .collect(joining("\n"))) + .build(); + } + + private String toSuccessLine(final Map.Entry> c) { + final var result = c.getValue().getNow(null); + return c.getKey().name() + "," + result.status() + (result.message() != null ? ",\"" + escape(result.message()) + '"' : ""); + } + + private String escape(final String string) { + return string.replace("\"", "\\\"").replace("\n", "\\n"); + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthCheck.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthCheck.java new file mode 100644 index 00000000..3397e264 --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthCheck.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.health; + +import java.util.concurrent.CompletionStage; + +public interface HealthCheck { + /** + * @return identifier for this healthcheck. + */ + String name(); + + /** + * @return type to match when calling {@code /health?type=xxxx} endpoint. + */ + default String type() { + return "live"; + } + + /** + * IMPORTANT: ensure to implement some consistent timeouts and error handling if you deploy in kubernetes to avoid to hang any healthcheck or require fusion to cancel them. + * + * @return the implementation of the check itself, key point is to return the status in the result. + */ + CompletionStage check(); + + record Result(Status status, String message) { + } + + enum Status { + OK, KO + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthRegistry.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthRegistry.java new file mode 100644 index 00000000..e484f39f --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/HealthRegistry.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.health; + +import io.yupiik.fusion.framework.api.scope.ApplicationScoped; +import io.yupiik.fusion.http.server.impl.health.HealthCheck; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +@ApplicationScoped +public class HealthRegistry { + private final List healthChecks; + + protected HealthRegistry() { + this(null); + } + + public HealthRegistry(final List healthChecks) { + this.healthChecks = healthChecks == null ? null : new CopyOnWriteArrayList<>(healthChecks); + } + + public Removable register(final HealthCheck check) { + healthChecks.add(check); + return () -> healthChecks.remove(check); + } + + public List healthChecks() { + return healthChecks; + } + + public interface Removable extends AutoCloseable { + @Override + void close(); + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java new file mode 100644 index 00000000..04b6c44d --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.http; + +import io.yupiik.fusion.framework.api.scope.DefaultScoped; +import io.yupiik.fusion.http.server.spi.Endpoint; +import io.yupiik.fusion.http.server.impl.health.Health; +import io.yupiik.fusion.http.server.impl.health.HealthRegistry; +import io.yupiik.fusion.http.server.impl.metrics.Metrics; +import io.yupiik.fusion.http.server.impl.metrics.MetricsRegistry; + +import java.util.List; + +@DefaultScoped +public class MonitoringEndpointRegistry { + private final List endpoints; + + public MonitoringEndpointRegistry(final HealthRegistry health, final MetricsRegistry metrics) { + this.endpoints = List.of(new Health(health), new Metrics(metrics)); + } + + public List endpoints() { + return endpoints; + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java new file mode 100644 index 00000000..c368401f --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.http; + +import io.yupiik.fusion.framework.api.configuration.Configuration; +import io.yupiik.fusion.framework.api.scope.ApplicationScoped; +import io.yupiik.fusion.framework.build.api.event.OnEvent; +import io.yupiik.fusion.http.server.api.WebServer; +import io.yupiik.fusion.http.server.impl.servlet.FusionServlet; +import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer; +import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServerConfiguration; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardEngine; +import org.apache.catalina.core.StandardHost; +import org.apache.catalina.core.StandardService; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.AbstractProtocol; + +import java.util.Set; +import java.util.stream.Stream; + +import static io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer.createBaseContext; + +@ApplicationScoped +public class ObservabilityServer { + private final Configuration configuration; + private final MonitoringEndpointRegistry registry; + private int port = -1; + + protected ObservabilityServer() { + this(null, null); + } + + public ObservabilityServer(final Configuration configuration, final MonitoringEndpointRegistry registry) { + this.configuration = configuration; + this.registry = registry; + } + + public int getPort() { + return port; + } + + public void onWebServerConfiguration(@OnEvent final WebServer.Configuration configuration) { + final var tomcatWebServerConfiguration = configuration.unwrap(TomcatWebServerConfiguration.class); + final var customizers = tomcatWebServerConfiguration.getTomcatCustomizers(); + tomcatWebServerConfiguration.setTomcatCustomizers( + Stream.concat( + customizers != null ? customizers.stream() : Stream.empty(), + Stream.of(t -> addObservabilityServer(t, tomcatWebServerConfiguration))) + .toList()); + } + + protected void addObservabilityServer(final Tomcat tomcat, final TomcatWebServerConfiguration webConf) { + final var host = new StandardHost(); + host.setAutoDeploy(false); + host.setName("localhost"); + host.addChild(newContext(webConf)); + + final var engine = new StandardEngine(); + engine.setName("Monitoring"); + engine.setDefaultHost(host.getName()); + engine.addChild(host); + + final var connector = new Connector() { + @Override + protected void startInternal() throws LifecycleException { + super.startInternal(); + if (getProtocolHandler() instanceof AbstractProtocol ap) { + port = ap.getLocalPort(); + } + } + }; + connector.setPort(this.configuration.get("fusion.observability.server.port") + .map(Integer::parseInt) + .orElse(8181)); + + final var service = new StandardService(); + service.setName("Observability"); + service.addConnector(connector); + service.setContainer(engine); + + tomcat.getServer().addService(service); + } + + protected Context newContext(final TomcatWebServerConfiguration webConf) { + final var baseContext = createBaseContext(new TomcatWebServer.NoWorkDirContext(), webConf); + baseContext.addServletContainerInitializer((ignored, ctx) -> { + final var observability = ctx.addServlet("observability", new FusionServlet(registry.endpoints())); + observability.setAsyncSupported(true); + observability.setLoadOnStartup(1); + observability.addMapping("/*"); + }, Set.of()); + return baseContext; + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java new file mode 100644 index 00000000..94388f2c --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.metrics; + +import io.yupiik.fusion.http.server.api.Request; +import io.yupiik.fusion.http.server.api.Response; +import io.yupiik.fusion.http.server.spi.Endpoint; + +import java.util.concurrent.CompletionStage; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +public class Metrics implements Endpoint { + private final MetricsRegistry registry; + private final OpenMetricsFormatter formatter = new OpenMetricsFormatter(); + + public Metrics(final MetricsRegistry registry) { + this.registry = registry; + } + + @Override + public boolean matches(final Request request) { + return "GET".equalsIgnoreCase(request.method()) && "/metrics".equalsIgnoreCase(request.path()); + } + + @Override + public CompletionStage handle(final Request request) { + request.setAttribute("skip-access-log", true); + return completedFuture(Response.of() + .status(200) + .header("content-type", "text/plain") + .body(formatter.apply(registry.entries())) + .build()); + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/MetricsRegistry.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/MetricsRegistry.java new file mode 100644 index 00000000..9152d189 --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/MetricsRegistry.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.metrics; + +import io.yupiik.fusion.framework.api.scope.ApplicationScoped; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.LongSupplier; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; + +@ApplicationScoped +public class MetricsRegistry { + private final Map counters = new ConcurrentHashMap<>(); + private final Map gauges = new ConcurrentHashMap<>(); + + public boolean isEmpty() { + return counters.isEmpty() && gauges.isEmpty(); + } + + public int getStatisticsSize() { + return counters.size() + gauges.size(); + } + + public Stream> entries() { + return Stream.concat( + counters.entrySet().stream(), + gauges.entrySet().stream()); + } + + public MetricsRegistry registerReadOnlyGauge(final String name, final String unit, final LongSupplier supplier) { + if (counters.containsKey(name) || gauges.put(name, new RegisteredMetric(name, "gauge", null, supplier, unit)) != null) { + throw new IllegalArgumentException("'" + name + "' gauge already registered"); + } + return this; + } + + public LongAdder getOrCreateMovingGauge(final String name, final String unit) { + if (counters.containsKey(name)) { + throw new IllegalArgumentException("'" + name + "' gauge already registered"); + } + return requireNonNull(gauges.computeIfAbsent(name, k -> { + final var longAdder = new LongAdder(); + return new RegisteredMetric(name, "gauge", longAdder, new LongAdderEvaluator(longAdder), unit); + }).adder(), "Can't use getOrCreateMovingGauge on a read only gauge"); + } + + public LongAdder registerCounter(final String name, final String unit) { + if (gauges.containsKey(name)) { + throw new IllegalArgumentException("'" + name + "' counter already registered"); + } + final var adder = new LongAdder(); + if (counters.put(name, new RegisteredMetric(name, "counter", adder, new LongAdderEvaluator(adder), unit)) != null) { + throw new IllegalArgumentException("'" + name + "' counter already registered"); + } + return adder; + } + + public void unregisterCounter(final String name) { + counters.remove(name); + } + + public void unregisterGauge(final String name) { + gauges.remove(name); + } + + private static class LongAdderEvaluator implements LongSupplier, Supplier { + private final LongAdder delegate; + + private LongAdderEvaluator(final LongAdder adder) { + this.delegate = adder; + } + + @Override + public long getAsLong() { + return delegate.sum(); + } + + @Override + public LongAdder get() { + return delegate; + } + } + + public record RegisteredMetric(LongAdder adder, LongSupplier supplier, String typeLine, String unitLine) { + private RegisteredMetric(final String name, + final String type, + final LongAdder adder, + final LongSupplier supplier, + final String unit) { + this(adder, supplier, + "# TYPE " + getMetaName(name) + " " + type, + unit == null ? null : "# UNIT " + getMetaName(name) + " " + unit); + } + + private static String getMetaName(final String name) { + final int tagStart = name.indexOf('{'); + return tagStart > 0 ? name.substring(0, tagStart) : name; + } + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/OpenMetricsFormatter.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/OpenMetricsFormatter.java new file mode 100644 index 00000000..8704cc2a --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/OpenMetricsFormatter.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.http.server.impl.metrics; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.joining; + +public class OpenMetricsFormatter implements Function>, String> { + @Override + public String apply(final Stream> entries) { + return entries + .sorted(Map.Entry.comparingByKey()) + .flatMap(it -> toOpenMetrics(it.getKey(), it.getValue())) + .collect(joining("\n", "", "\n# EOF")); + } + + private Stream toOpenMetrics(final String name, final MetricsRegistry.RegisteredMetric metric) { + return Stream.of( + metric.typeLine(), + metric.unitLine(), + name + " " + metric.supplier().getAsLong()) + .filter(Objects::nonNull); + } +} diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java index e76a1015..2a931197 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.health; +package io.yupiik.fusion.http.server.impl.health; import io.yupiik.fusion.http.server.api.Request; import io.yupiik.fusion.http.server.api.Response; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java index 55f047c6..3397e264 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.health; +package io.yupiik.fusion.http.server.impl.health; import java.util.concurrent.CompletionStage; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java index 7f735161..afa31751 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.health; +package io.yupiik.fusion.http.server.impl.health; import io.yupiik.fusion.framework.api.scope.ApplicationScoped; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java index 1dca584b..04b6c44d 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java @@ -13,14 +13,14 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.http; +package io.yupiik.fusion.http.server.impl.http; import io.yupiik.fusion.framework.api.scope.DefaultScoped; import io.yupiik.fusion.http.server.spi.Endpoint; -import io.yupiik.fusion.observability.health.Health; -import io.yupiik.fusion.observability.health.HealthRegistry; -import io.yupiik.fusion.observability.metrics.Metrics; -import io.yupiik.fusion.observability.metrics.MetricsRegistry; +import io.yupiik.fusion.http.server.impl.health.Health; +import io.yupiik.fusion.http.server.impl.health.HealthRegistry; +import io.yupiik.fusion.http.server.impl.metrics.Metrics; +import io.yupiik.fusion.http.server.impl.metrics.MetricsRegistry; import java.util.List; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java index 43f2b81a..c368401f 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.http; +package io.yupiik.fusion.http.server.impl.http; import io.yupiik.fusion.framework.api.configuration.Configuration; import io.yupiik.fusion.framework.api.scope.ApplicationScoped; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java index 45152df8..94388f2c 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.metrics; +package io.yupiik.fusion.http.server.impl.metrics; import io.yupiik.fusion.http.server.api.Request; import io.yupiik.fusion.http.server.api.Response; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java index e99edc59..9152d189 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.metrics; +package io.yupiik.fusion.http.server.impl.metrics; import io.yupiik.fusion.framework.api.scope.ApplicationScoped; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java index 1fc04992..8704cc2a 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.observability.metrics; +package io.yupiik.fusion.http.server.impl.metrics; import java.util.Map; import java.util.Objects; From a27b91bc6862e2884c906298b1d02734dd3ebde8 Mon Sep 17 00:00:00 2001 From: Anukalp Date: Thu, 29 Jan 2026 20:02:15 +0530 Subject: [PATCH 2/6] Remove redundant fusion-observability module --- .../io/yupiik/fusion/observability/health/Health.java | 2 +- .../fusion/observability/health/HealthCheck.java | 2 +- .../fusion/observability/health/HealthRegistry.java | 2 +- .../observability/http/MonitoringEndpointRegistry.java | 10 +++++----- .../fusion/observability/http/ObservabilityServer.java | 2 +- .../yupiik/fusion/observability/metrics/Metrics.java | 2 +- .../fusion/observability/metrics/MetricsRegistry.java | 2 +- .../observability/metrics/OpenMetricsFormatter.java | 2 +- pom.xml | 1 - 9 files changed, 12 insertions(+), 13 deletions(-) diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java index 2a931197..e76a1015 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/Health.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.health; +package io.yupiik.fusion.observability.health; import io.yupiik.fusion.http.server.api.Request; import io.yupiik.fusion.http.server.api.Response; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java index 3397e264..55f047c6 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthCheck.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.health; +package io.yupiik.fusion.observability.health; import java.util.concurrent.CompletionStage; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java index afa31751..7f735161 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.health; +package io.yupiik.fusion.observability.health; import io.yupiik.fusion.framework.api.scope.ApplicationScoped; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java index 04b6c44d..1dca584b 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/MonitoringEndpointRegistry.java @@ -13,14 +13,14 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.http; +package io.yupiik.fusion.observability.http; import io.yupiik.fusion.framework.api.scope.DefaultScoped; import io.yupiik.fusion.http.server.spi.Endpoint; -import io.yupiik.fusion.http.server.impl.health.Health; -import io.yupiik.fusion.http.server.impl.health.HealthRegistry; -import io.yupiik.fusion.http.server.impl.metrics.Metrics; -import io.yupiik.fusion.http.server.impl.metrics.MetricsRegistry; +import io.yupiik.fusion.observability.health.Health; +import io.yupiik.fusion.observability.health.HealthRegistry; +import io.yupiik.fusion.observability.metrics.Metrics; +import io.yupiik.fusion.observability.metrics.MetricsRegistry; import java.util.List; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java index c368401f..43f2b81a 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.http; +package io.yupiik.fusion.observability.http; import io.yupiik.fusion.framework.api.configuration.Configuration; import io.yupiik.fusion.framework.api.scope.ApplicationScoped; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java index 94388f2c..45152df8 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.metrics; +package io.yupiik.fusion.observability.metrics; import io.yupiik.fusion.http.server.api.Request; import io.yupiik.fusion.http.server.api.Response; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java index 9152d189..e99edc59 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.metrics; +package io.yupiik.fusion.observability.metrics; import io.yupiik.fusion.framework.api.scope.ApplicationScoped; diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java index 8704cc2a..1fc04992 100644 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java +++ b/fusion-observability/src/main/java/io/yupiik/fusion/observability/metrics/OpenMetricsFormatter.java @@ -13,7 +13,7 @@ * specific language governing permissions and limitations * under the License. */ -package io.yupiik.fusion.http.server.impl.metrics; +package io.yupiik.fusion.observability.metrics; import java.util.Map; import java.util.Objects; diff --git a/pom.xml b/pom.xml index 34d16f42..a5d7cefd 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,6 @@ fusion-cli fusion-httpclient-parent fusion-tracing - fusion-observability fusion-persistence fusion-documentation fusion-handlebars From 774c758a8e6faaf909e321b73ac0aaecdfecf814 Mon Sep 17 00:00:00 2001 From: Anukalp Date: Fri, 30 Jan 2026 00:21:30 +0530 Subject: [PATCH 3/6] Align monitoring endpoints and preserve legacy observability API --- .../http/server/impl/health/Health.java | 4 ++-- .../impl/http/MonitoringEndpointRegistry.java | 6 ++--- .../http/server/impl/metrics/Metrics.java | 4 ++-- .../fusion/observability/health/Health.java | 22 +++++++++++++++++++ .../observability/health/HealthRegistry.java | 20 +++++++++++++++++ .../fusion/observability/metrics/Metrics.java | 22 +++++++++++++++++++ .../metrics/MetricsRegistry.java | 20 +++++++++++++++++ 7 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/Health.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java create mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java index 31a1ff16..9f16d8f9 100644 --- a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/health/Health.java @@ -17,7 +17,7 @@ import io.yupiik.fusion.http.server.api.Request; import io.yupiik.fusion.http.server.api.Response; -import io.yupiik.fusion.http.server.spi.Endpoint; +import io.yupiik.fusion.http.server.spi.MonitoringEndpoint; import io.yupiik.fusion.http.server.impl.health.HealthCheck; import io.yupiik.fusion.http.server.impl.health.HealthRegistry; @@ -34,7 +34,7 @@ import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toMap; -public class Health implements Endpoint { +public class Health implements MonitoringEndpoint { private final HealthRegistry healthChecks; public Health(final HealthRegistry healthChecks) { diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java index 04b6c44d..0ed67fcc 100644 --- a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/MonitoringEndpointRegistry.java @@ -16,7 +16,7 @@ package io.yupiik.fusion.http.server.impl.http; import io.yupiik.fusion.framework.api.scope.DefaultScoped; -import io.yupiik.fusion.http.server.spi.Endpoint; +import io.yupiik.fusion.http.server.spi.MonitoringEndpoint; import io.yupiik.fusion.http.server.impl.health.Health; import io.yupiik.fusion.http.server.impl.health.HealthRegistry; import io.yupiik.fusion.http.server.impl.metrics.Metrics; @@ -26,13 +26,13 @@ @DefaultScoped public class MonitoringEndpointRegistry { - private final List endpoints; + private final List endpoints; public MonitoringEndpointRegistry(final HealthRegistry health, final MetricsRegistry metrics) { this.endpoints = List.of(new Health(health), new Metrics(metrics)); } - public List endpoints() { + public List endpoints() { return endpoints; } } diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java index 94388f2c..dc7e74c7 100644 --- a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/metrics/Metrics.java @@ -17,13 +17,13 @@ import io.yupiik.fusion.http.server.api.Request; import io.yupiik.fusion.http.server.api.Response; -import io.yupiik.fusion.http.server.spi.Endpoint; +import io.yupiik.fusion.http.server.spi.MonitoringEndpoint; import java.util.concurrent.CompletionStage; import static java.util.concurrent.CompletableFuture.completedFuture; -public class Metrics implements Endpoint { +public class Metrics implements MonitoringEndpoint { private final MetricsRegistry registry; private final OpenMetricsFormatter formatter = new OpenMetricsFormatter(); diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/Health.java b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/Health.java new file mode 100644 index 00000000..2ccf26e2 --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/Health.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.observability.health; + +public class Health extends io.yupiik.fusion.http.server.impl.health.Health { + public Health(final HealthRegistry registry) { + super(registry); + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java new file mode 100644 index 00000000..de10e785 --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/health/HealthRegistry.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.observability.health; + +public class HealthRegistry extends + io.yupiik.fusion.http.server.impl.health.HealthRegistry { +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java new file mode 100644 index 00000000..5110d190 --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/Metrics.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.observability.metrics; + +public class Metrics extends io.yupiik.fusion.http.server.impl.metrics.Metrics { + public Metrics(final MetricsRegistry registry) { + super(registry); + } +} diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java new file mode 100644 index 00000000..bf98a14d --- /dev/null +++ b/fusion-http-server/src/main/java/io/yupiik/fusion/observability/metrics/MetricsRegistry.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.observability.metrics; + +public class MetricsRegistry extends + io.yupiik.fusion.http.server.impl.metrics.MetricsRegistry { +} From ceb3720e9058e4e47e2cda939f8364d39328a979 Mon Sep 17 00:00:00 2001 From: Anukalp Date: Fri, 30 Jan 2026 08:25:12 +0530 Subject: [PATCH 4/6] Remove duplicated observability server and rely on http-server monitoring --- .../server/impl/http/ObservabilityServer.java | 110 --------------- .../http/ObservabilityServer.java | 110 --------------- .../http/ObservabilityServerTest.java | 133 ------------------ 3 files changed, 353 deletions(-) delete mode 100644 fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java delete mode 100644 fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java delete mode 100644 fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java diff --git a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java b/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java deleted file mode 100644 index c368401f..00000000 --- a/fusion-http-server/src/main/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServer.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package io.yupiik.fusion.http.server.impl.http; - -import io.yupiik.fusion.framework.api.configuration.Configuration; -import io.yupiik.fusion.framework.api.scope.ApplicationScoped; -import io.yupiik.fusion.framework.build.api.event.OnEvent; -import io.yupiik.fusion.http.server.api.WebServer; -import io.yupiik.fusion.http.server.impl.servlet.FusionServlet; -import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer; -import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServerConfiguration; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.core.StandardEngine; -import org.apache.catalina.core.StandardHost; -import org.apache.catalina.core.StandardService; -import org.apache.catalina.startup.Tomcat; -import org.apache.coyote.AbstractProtocol; - -import java.util.Set; -import java.util.stream.Stream; - -import static io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer.createBaseContext; - -@ApplicationScoped -public class ObservabilityServer { - private final Configuration configuration; - private final MonitoringEndpointRegistry registry; - private int port = -1; - - protected ObservabilityServer() { - this(null, null); - } - - public ObservabilityServer(final Configuration configuration, final MonitoringEndpointRegistry registry) { - this.configuration = configuration; - this.registry = registry; - } - - public int getPort() { - return port; - } - - public void onWebServerConfiguration(@OnEvent final WebServer.Configuration configuration) { - final var tomcatWebServerConfiguration = configuration.unwrap(TomcatWebServerConfiguration.class); - final var customizers = tomcatWebServerConfiguration.getTomcatCustomizers(); - tomcatWebServerConfiguration.setTomcatCustomizers( - Stream.concat( - customizers != null ? customizers.stream() : Stream.empty(), - Stream.of(t -> addObservabilityServer(t, tomcatWebServerConfiguration))) - .toList()); - } - - protected void addObservabilityServer(final Tomcat tomcat, final TomcatWebServerConfiguration webConf) { - final var host = new StandardHost(); - host.setAutoDeploy(false); - host.setName("localhost"); - host.addChild(newContext(webConf)); - - final var engine = new StandardEngine(); - engine.setName("Monitoring"); - engine.setDefaultHost(host.getName()); - engine.addChild(host); - - final var connector = new Connector() { - @Override - protected void startInternal() throws LifecycleException { - super.startInternal(); - if (getProtocolHandler() instanceof AbstractProtocol ap) { - port = ap.getLocalPort(); - } - } - }; - connector.setPort(this.configuration.get("fusion.observability.server.port") - .map(Integer::parseInt) - .orElse(8181)); - - final var service = new StandardService(); - service.setName("Observability"); - service.addConnector(connector); - service.setContainer(engine); - - tomcat.getServer().addService(service); - } - - protected Context newContext(final TomcatWebServerConfiguration webConf) { - final var baseContext = createBaseContext(new TomcatWebServer.NoWorkDirContext(), webConf); - baseContext.addServletContainerInitializer((ignored, ctx) -> { - final var observability = ctx.addServlet("observability", new FusionServlet(registry.endpoints())); - observability.setAsyncSupported(true); - observability.setLoadOnStartup(1); - observability.addMapping("/*"); - }, Set.of()); - return baseContext; - } -} diff --git a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java b/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java deleted file mode 100644 index 43f2b81a..00000000 --- a/fusion-observability/src/main/java/io/yupiik/fusion/observability/http/ObservabilityServer.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package io.yupiik.fusion.observability.http; - -import io.yupiik.fusion.framework.api.configuration.Configuration; -import io.yupiik.fusion.framework.api.scope.ApplicationScoped; -import io.yupiik.fusion.framework.build.api.event.OnEvent; -import io.yupiik.fusion.http.server.api.WebServer; -import io.yupiik.fusion.http.server.impl.servlet.FusionServlet; -import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer; -import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServerConfiguration; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.core.StandardEngine; -import org.apache.catalina.core.StandardHost; -import org.apache.catalina.core.StandardService; -import org.apache.catalina.startup.Tomcat; -import org.apache.coyote.AbstractProtocol; - -import java.util.Set; -import java.util.stream.Stream; - -import static io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer.createBaseContext; - -@ApplicationScoped -public class ObservabilityServer { - private final Configuration configuration; - private final MonitoringEndpointRegistry registry; - private int port = -1; - - protected ObservabilityServer() { - this(null, null); - } - - public ObservabilityServer(final Configuration configuration, final MonitoringEndpointRegistry registry) { - this.configuration = configuration; - this.registry = registry; - } - - public int getPort() { - return port; - } - - public void onWebServerConfiguration(@OnEvent final WebServer.Configuration configuration) { - final var tomcatWebServerConfiguration = configuration.unwrap(TomcatWebServerConfiguration.class); - final var customizers = tomcatWebServerConfiguration.getTomcatCustomizers(); - tomcatWebServerConfiguration.setTomcatCustomizers( - Stream.concat( - customizers != null ? customizers.stream() : Stream.empty(), - Stream.of(t -> addObservabilityServer(t, tomcatWebServerConfiguration))) - .toList()); - } - - protected void addObservabilityServer(final Tomcat tomcat, final TomcatWebServerConfiguration webConf) { - final var host = new StandardHost(); - host.setAutoDeploy(false); - host.setName("localhost"); - host.addChild(newContext(webConf)); - - final var engine = new StandardEngine(); - engine.setName("Monitoring"); - engine.setDefaultHost(host.getName()); - engine.addChild(host); - - final var connector = new Connector() { - @Override - protected void startInternal() throws LifecycleException { - super.startInternal(); - if (getProtocolHandler() instanceof AbstractProtocol ap) { - port = ap.getLocalPort(); - } - } - }; - connector.setPort(this.configuration.get("fusion.observability.server.port") - .map(Integer::parseInt) - .orElse(8181)); - - final var service = new StandardService(); - service.setName("Observability"); - service.addConnector(connector); - service.setContainer(engine); - - tomcat.getServer().addService(service); - } - - protected Context newContext(final TomcatWebServerConfiguration webConf) { - final var baseContext = createBaseContext(new TomcatWebServer.NoWorkDirContext(), webConf); - baseContext.addServletContainerInitializer((ignored, ctx) -> { - final var observability = ctx.addServlet("observability", new FusionServlet(registry.endpoints())); - observability.setAsyncSupported(true); - observability.setLoadOnStartup(1); - observability.addMapping("/*"); - }, Set.of()); - return baseContext; - } -} diff --git a/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java b/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java deleted file mode 100644 index 11eb848a..00000000 --- a/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package io.yupiik.fusion.observability.http; - -import io.yupiik.fusion.http.server.api.WebServer; -import io.yupiik.fusion.observability.health.HealthCheck; -import io.yupiik.fusion.observability.health.HealthRegistry; -import io.yupiik.fusion.observability.http.test.SampleCheck; -import io.yupiik.fusion.observability.metrics.MetricsRegistry; -import io.yupiik.fusion.testing.Fusion; -import io.yupiik.fusion.testing.FusionSupport; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.concurrent.CompletableFuture; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; - -@FusionSupport -@TestInstance(PER_CLASS) -class ObservabilityServerTest { - private final HttpClient client = HttpClient.newHttpClient(); - - @Test - void metrics(@Fusion final ObservabilityServer server, @Fusion final WebServer webServer, - @Fusion final MetricsRegistry registry) throws IOException, InterruptedException { - registry.registerReadOnlyGauge("my_gauge", "value", () -> 100); - { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/metrics")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode()); - assertEquals(""" - # TYPE my_gauge gauge - # UNIT my_gauge value - my_gauge 100 - # EOF""", - response.body()); - } - { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + webServer.configuration().port() + "/metrics")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(404, response.statusCode()); - } - registry.unregisterGauge("my_gauge"); - } - - @Test - void health(@Fusion final ObservabilityServer server, @Fusion final WebServer webServer, - @Fusion final HealthRegistry registry, - @Fusion final SampleCheck check) throws IOException, InterruptedException { - { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode()); - assertEquals("test-check,OK,\"worked\"", response.body()); - } - - final var oldCheck = check.check(); - - final var failure = new CompletableFuture(); - failure.completeExceptionally(new IllegalStateException("oops")); - check.setCheck(failure); - try { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(503, response.statusCode()); - assertEquals("test-check,KO,\"java.lang.IllegalStateException: oops\"", response.body()); - } finally { - check.setCheck(oldCheck); - } - - final var ko = new CompletableFuture(); - ko.complete(new HealthCheck.Result(HealthCheck.Status.KO, "oops from test")); - check.setCheck(ko); - try { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(503, response.statusCode()); - assertEquals("test-check,KO,\"oops from test\"", response.body()); - } finally { - check.setCheck(oldCheck); - } - - { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + webServer.configuration().port() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(404, response.statusCode()); - } - } -} From c45ba06919f8d562f64938d383e7977a94ba524d Mon Sep 17 00:00:00 2001 From: Anukalp Date: Fri, 30 Jan 2026 18:59:50 +0530 Subject: [PATCH 5/6] Merge observability into http-server and update tests and documentation --- fusion-documentation/pom.xml | 2 +- .../http/ObservabilityServerTest.java | 179 ++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java diff --git a/fusion-documentation/pom.xml b/fusion-documentation/pom.xml index 89c2e398..54a306f2 100644 --- a/fusion-documentation/pom.xml +++ b/fusion-documentation/pom.xml @@ -64,7 +64,7 @@ ${project.groupId} - fusion-observability + fusion-http-server ${project.version} provided diff --git a/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java b/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java new file mode 100644 index 00000000..c6582721 --- /dev/null +++ b/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package io.yupiik.fusion.observability.http; + +import io.yupiik.fusion.http.server.api.WebServer; +import io.yupiik.fusion.observability.health.HealthCheck; +import io.yupiik.fusion.observability.health.HealthRegistry; +import io.yupiik.fusion.observability.http.test.SampleCheck; +import io.yupiik.fusion.observability.metrics.MetricsRegistry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; + +import io.yupiik.fusion.http.server.impl.health.Health; +import io.yupiik.fusion.http.server.impl.tomcat.MonitoringServerConfiguration; +import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer; +import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServerConfiguration; + +import java.util.List; + +// This test used to rely on Fusion/CDI injection. +// Since http-server does not use the processor, the server and monitoring +// endpoints are now wired explicitly to reflect real runtime behavior. +class ObservabilityServerTest { + private final HttpClient client = HttpClient.newHttpClient(); + + @Test + void metrics() throws IOException, InterruptedException { + final var registry = new MetricsRegistry(); + registry.registerReadOnlyGauge("my_gauge", "value", () -> 100); + + // Explicitly configure monitoring endpoints instead of relying on CDI/Fusion + final var monitoring = new MonitoringServerConfiguration() + .setport(0) + .setEndpoints(List.of(new Metrics(registry))); + + final var configuration = new TomcatWebServerConfiguration() + .setPort(0) + .setMonitoringServerConfiguration(monitoring); + + // Start a real Tomcat server to validate monitoring behavior at runtime + try (var server = new TomcatWebServer(configuration)) { + server.start(); + + final var port = server.getConfiguration() + .getMonitoringServerConfiguration() + .getPort(); + + { + final var response = client.send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create("http://localhost:" + server.getPort() + "/metrics")) + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + assertEquals(""" + # TYPE my_gauge gauge + # UNIT my_gauge value + my_gauge 100 + # EOF""", + response.body()); + } + { + // Ensure monitoring endpoints are not exposed on the main HTTP server + final var response = client.send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create("http://localhost:" + webServer.configuration().port() + "/metrics")) + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(404, response.statusCode()); + } + finally{ + registry.unregisterGauge("my_gauge"); + } + } + + @Test + void health() throws IOException, InterruptedException { + + final var check = new SampleCheck(); + final var registry = new HealthRegistry(); + registry.register(check); + + final var monitoring = new MonitoringServerConfiguration() + .setPort(0) + .setEndpoints(List.of(new Health(registry))); + + final var configuration = new TomcatWebServerConfiguration() + .setPort(0) + .setMonitoringServerConfiguration(monitoring); + + try (var server = new TomcatWebServer(configuration)) { + server.start(); + + final var port = server.getConfiguration() + .getMonitoringServerConfiguration() + .getPort(); + + { + final var response = client.send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create("http://localhost:" + server.getPort() + "/health")) + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + assertEquals("test-check,OK,\"worked\"", response.body()); + } + + // Validate health behavior for failing and KO checks + final var oldCheck = check.check(); + + final var failure = new CompletableFuture(); + failure.completeExceptionally(new IllegalStateException("oops")); + check.setCheck(failure); + try { + final var response = client.send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create("http://localhost:" + server.getPort() + "/health")) + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(503, response.statusCode()); + assertEquals("test-check,KO,\"java.lang.IllegalStateException: oops\"", response.body()); + } finally { + check.setCheck(oldCheck); + } + + final var ko = new CompletableFuture(); + ko.complete(new HealthCheck.Result(HealthCheck.Status.KO, "oops from test")); + check.setCheck(ko); + try { + final var response = client.send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create("http://localhost:" + server.getPort() + "/health")) + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(503, response.statusCode()); + assertEquals("test-check,KO,\"oops from test\"", response.body()); + } finally { + check.setCheck(oldCheck); + } + + { + final var response = client.send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create("http://localhost:" + webServer.configuration().port() + "/health")) + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(404, response.statusCode()); + } + } +} From fe9fcb9d86ca0bf55d2dd3395ca90fd33a6bf1a0 Mon Sep 17 00:00:00 2001 From: Anukalp Date: Fri, 30 Jan 2026 21:23:41 +0530 Subject: [PATCH 6/6] Move observability tests to http-server and validate monitoring runtime behavior --- .../impl/http/ObservabilityServerTest.java | 138 ++++++++++++++ .../http/ObservabilityServerTest.java | 179 ------------------ 2 files changed, 138 insertions(+), 179 deletions(-) create mode 100644 fusion-http-server/src/test/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServerTest.java delete mode 100644 fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java diff --git a/fusion-http-server/src/test/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServerTest.java b/fusion-http-server/src/test/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServerTest.java new file mode 100644 index 00000000..a58591df --- /dev/null +++ b/fusion-http-server/src/test/java/io/yupiik/fusion/http/server/impl/http/ObservabilityServerTest.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + + +package io.yupiik.fusion.http.server.impl.http; + +import io.yupiik.fusion.http.server.impl.health.Health; +import io.yupiik.fusion.http.server.impl.health.HealthCheck; +import io.yupiik.fusion.http.server.impl.health.HealthRegistry; +import io.yupiik.fusion.http.server.impl.metrics.Metrics; +import io.yupiik.fusion.http.server.impl.tomcat.MonitoringServerConfiguration; +import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer; +import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServerConfiguration; +import io.yupiik.fusion.observability.metrics.MetricsRegistry; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/** + * Runtime tests for observability endpoints exposed by the HTTP server. + * + *

+ * These tests intentionally avoid Fusion/CDI and the build processor. + * The http-server module wires monitoring endpoints explicitly at runtime, + * so the tests reflect the actual production behavior. + *

+ */ +class ObservabilityServerTest { + + // Plain JDK client to exercise the server as a real consumer would + private final HttpClient client = HttpClient.newHttpClient(); + + @Test + void metrics() throws Exception { + // Metrics registry is created explicitly: + // http-server does not rely on the processor / CDI at runtime + final var registry = new MetricsRegistry(); + registry.registerReadOnlyGauge("my_gauge", "value", () -> 100); + + // Monitoring endpoints are wired manually to reflect real runtime usage + final var monitoring = new MonitoringServerConfiguration() + .setPort(0) // let the OS choose a free port + .setEndpoints(List.of(new Metrics(registry))); + + final var configuration = new TomcatWebServerConfiguration(); + configuration.setPort(0); // main HTTP server port + configuration.setMonitoringServerConfiguration(monitoring); + + // Start a real Tomcat instance to validate monitoring behavior end-to-end + try (var server = new TomcatWebServer(configuration)) { + final int monitoringPort = monitoring.getPort(); + + final var response = client.send( + HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + monitoringPort + "/metrics")) + .GET() + .build(), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + assertTrue(response.body().contains("my_gauge")); + } finally { + // Cleanup to avoid leaking state across tests + registry.unregisterGauge("my_gauge"); + } + } + + @Test + void health() throws Exception { + // HealthCheck implemented inline to avoid relying on observability internals + final HealthCheck okCheck = new HealthCheck() { + @Override + public String name() { + return "test-check"; + } + + @Override + public CompletableFuture check() { + return CompletableFuture.completedFuture( + new Result(Status.OK, "worked")); + } + }; + + // HealthRegistry is overridden to expose a fixed set of checks + // (no mutation, no internal field access) + final HealthRegistry registry = new HealthRegistry() { + @Override + public List healthChecks() { + return List.of(okCheck); + } + }; + + final var monitoring = new MonitoringServerConfiguration() + .setPort(0) + .setEndpoints(List.of(new Health(registry))); + + final var configuration = new TomcatWebServerConfiguration(); + configuration.setPort(0); + configuration.setMonitoringServerConfiguration(monitoring); + + try (var server = new TomcatWebServer(configuration)) { + final int monitoringPort = monitoring.getPort(); + + final var response = client.send( + HttpRequest.newBuilder() + .GET() + .uri(URI.create("http://localhost:" + monitoringPort + "/health")) + .build(), + HttpResponse.BodyHandlers.ofString()); + + assertEquals(200, response.statusCode()); + assertEquals("test-check,OK,\"worked\"", response.body()); + } + } +} diff --git a/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java b/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java deleted file mode 100644 index c6582721..00000000 --- a/fusion-observability/src/test/java/io/yupiik/fusion/observability/http/ObservabilityServerTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2022 - present - Yupiik SAS - https://www.yupiik.com - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package io.yupiik.fusion.observability.http; - -import io.yupiik.fusion.http.server.api.WebServer; -import io.yupiik.fusion.observability.health.HealthCheck; -import io.yupiik.fusion.observability.health.HealthRegistry; -import io.yupiik.fusion.observability.http.test.SampleCheck; -import io.yupiik.fusion.observability.metrics.MetricsRegistry; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; - -import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.concurrent.CompletableFuture; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; - -import io.yupiik.fusion.http.server.impl.health.Health; -import io.yupiik.fusion.http.server.impl.tomcat.MonitoringServerConfiguration; -import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServer; -import io.yupiik.fusion.http.server.impl.tomcat.TomcatWebServerConfiguration; - -import java.util.List; - -// This test used to rely on Fusion/CDI injection. -// Since http-server does not use the processor, the server and monitoring -// endpoints are now wired explicitly to reflect real runtime behavior. -class ObservabilityServerTest { - private final HttpClient client = HttpClient.newHttpClient(); - - @Test - void metrics() throws IOException, InterruptedException { - final var registry = new MetricsRegistry(); - registry.registerReadOnlyGauge("my_gauge", "value", () -> 100); - - // Explicitly configure monitoring endpoints instead of relying on CDI/Fusion - final var monitoring = new MonitoringServerConfiguration() - .setport(0) - .setEndpoints(List.of(new Metrics(registry))); - - final var configuration = new TomcatWebServerConfiguration() - .setPort(0) - .setMonitoringServerConfiguration(monitoring); - - // Start a real Tomcat server to validate monitoring behavior at runtime - try (var server = new TomcatWebServer(configuration)) { - server.start(); - - final var port = server.getConfiguration() - .getMonitoringServerConfiguration() - .getPort(); - - { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/metrics")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode()); - assertEquals(""" - # TYPE my_gauge gauge - # UNIT my_gauge value - my_gauge 100 - # EOF""", - response.body()); - } - { - // Ensure monitoring endpoints are not exposed on the main HTTP server - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + webServer.configuration().port() + "/metrics")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(404, response.statusCode()); - } - finally{ - registry.unregisterGauge("my_gauge"); - } - } - - @Test - void health() throws IOException, InterruptedException { - - final var check = new SampleCheck(); - final var registry = new HealthRegistry(); - registry.register(check); - - final var monitoring = new MonitoringServerConfiguration() - .setPort(0) - .setEndpoints(List.of(new Health(registry))); - - final var configuration = new TomcatWebServerConfiguration() - .setPort(0) - .setMonitoringServerConfiguration(monitoring); - - try (var server = new TomcatWebServer(configuration)) { - server.start(); - - final var port = server.getConfiguration() - .getMonitoringServerConfiguration() - .getPort(); - - { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(200, response.statusCode()); - assertEquals("test-check,OK,\"worked\"", response.body()); - } - - // Validate health behavior for failing and KO checks - final var oldCheck = check.check(); - - final var failure = new CompletableFuture(); - failure.completeExceptionally(new IllegalStateException("oops")); - check.setCheck(failure); - try { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(503, response.statusCode()); - assertEquals("test-check,KO,\"java.lang.IllegalStateException: oops\"", response.body()); - } finally { - check.setCheck(oldCheck); - } - - final var ko = new CompletableFuture(); - ko.complete(new HealthCheck.Result(HealthCheck.Status.KO, "oops from test")); - check.setCheck(ko); - try { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + server.getPort() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(503, response.statusCode()); - assertEquals("test-check,KO,\"oops from test\"", response.body()); - } finally { - check.setCheck(oldCheck); - } - - { - final var response = client.send( - HttpRequest.newBuilder() - .GET() - .uri(URI.create("http://localhost:" + webServer.configuration().port() + "/health")) - .build(), - HttpResponse.BodyHandlers.ofString()); - assertEquals(404, response.statusCode()); - } - } -}