From 094dd4ac3e72a4630d785e6ea17c14e82bf6f7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Tue, 19 May 2026 17:40:35 +0200 Subject: [PATCH 1/2] feat(observability): auto-load application-firefly-observability.yml defaults FireflyObservabilityEnvironmentPostProcessor now loads the bundled application-firefly-observability.yml from the classpath and adds it to the environment with lowest precedence. Host application.yml always wins, but every Firefly-based service now gets sensible defaults out of the box: - Actuator endpoints exposed: health,info,metrics,prometheus - Kubernetes liveness/readiness probe groups - W3C+B3 composite trace propagation - OTLP endpoints (overridable via OTEL_EXPORTER_OTLP_* env vars) - Graceful shutdown (30s phase timeout) - Logback structured (logstash JSON) console output - Reactor Hooks.enableAutomaticContextPropagation() Added FireflyObservabilityEnvironmentPostProcessorTest with 7 tests: defaults loading, host-override precedence, OTEL/Brave switching, PROMETHEUS/OTLP/BOTH exporter switching, idempotence. Previously the YAML was registered as a Spring profile but never activated, so downstream services were silently missing the defaults until they opted in explicitly. This closes that integration gap. --- ...ObservabilityEnvironmentPostProcessor.java | 44 +++++- ...rvabilityEnvironmentPostProcessorTest.java | 130 ++++++++++++++++++ 2 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java diff --git a/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java b/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java index 0b98e7d..99948c6 100644 --- a/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java +++ b/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java @@ -1,13 +1,18 @@ package org.fireflyframework.observability; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Properties; /** * Configures observability backends at startup based on Firefly properties. @@ -56,6 +61,8 @@ public class FireflyObservabilityEnvironmentPostProcessor implements Environment private static final String OTLP_METRICS_ENABLED = "management.otlp.metrics.export.enabled"; private static final String PROPERTY_SOURCE_NAME = "fireflyObservabilityPostProcessor"; + private static final String DEFAULTS_SOURCE_NAME = "fireflyObservabilityDefaults"; + private static final String DEFAULTS_YAML = "application-firefly-observability.yml"; // Spring Boot actuator auto-configuration classes for each tracing bridge. // Both current (3.4+) and deprecated (pre-3.4) class names are excluded for maximum safety. @@ -72,17 +79,50 @@ public class FireflyObservabilityEnvironmentPostProcessor implements Environment @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + // 1. Load framework default observability properties (actuator endpoints, + // health groups, OTLP endpoints, etc.) — added with LOWEST precedence so the + // user's own application.yml always wins. + loadFrameworkDefaults(environment); + + // 2. Resolve the active tracing bridge & metrics exporter and install the + // corresponding Spring Boot auto-config exclusions / enable flags with + // HIGHEST precedence. Map props = new LinkedHashMap<>(); - configureTracingBridge(environment, props); configureMetricsExporter(environment, props); - if (!props.isEmpty()) { environment.getPropertySources().addFirst( new MapPropertySource(PROPERTY_SOURCE_NAME, props)); } } + /** + * Loads {@code application-firefly-observability.yml} from the classpath and adds + * its contents to the environment with the lowest precedence so that any property + * set by the host application overrides them. + *

+ * If the resource is missing or unreadable, the method silently no-ops — the + * framework still works, the host just won't get the default endpoint exposure, + * tracing settings, OTLP endpoints, etc., until they opt in explicitly. + */ + private void loadFrameworkDefaults(ConfigurableEnvironment environment) { + if (environment.getPropertySources().contains(DEFAULTS_SOURCE_NAME)) { + return; // idempotent — never double-load if invoked twice + } + Resource resource = new ClassPathResource(DEFAULTS_YAML); + if (!resource.exists()) { + return; + } + YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean(); + yaml.setResources(resource); + Properties properties = yaml.getObject(); + if (properties == null || properties.isEmpty()) { + return; + } + environment.getPropertySources().addLast( + new PropertiesPropertySource(DEFAULTS_SOURCE_NAME, properties)); + } + private void configureTracingBridge(ConfigurableEnvironment environment, Map props) { boolean tracingEnabled = environment.getProperty(TRACING_ENABLED_PROPERTY, Boolean.class, true); if (!tracingEnabled) { diff --git a/src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java b/src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java new file mode 100644 index 0000000..276102d --- /dev/null +++ b/src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2024-2026 Firefly Software Foundation + * + * 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 org.fireflyframework.observability.autoconfigure; + +import org.fireflyframework.observability.FireflyObservabilityEnvironmentPostProcessor; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +class FireflyObservabilityEnvironmentPostProcessorTest { + + private final FireflyObservabilityEnvironmentPostProcessor processor = + new FireflyObservabilityEnvironmentPostProcessor(); + + @Test + void defaultsAreLoadedFromYaml() { + MockEnvironment environment = new MockEnvironment(); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + // The bundled application-firefly-observability.yml should now be on the environment + // so downstream services get sensible actuator/OTLP defaults out of the box. + assertThat(environment.getProperty("management.endpoints.web.exposure.include")) + .as("Default actuator endpoint exposure must be applied from framework defaults") + .contains("prometheus"); + assertThat(environment.getProperty("management.endpoint.health.probes.enabled")) + .as("Kubernetes liveness/readiness probes must be enabled by default") + .isEqualTo("true"); + assertThat(environment.getProperty("management.tracing.propagation.type")) + .as("Trace propagation default should be composite W3C+B3") + .isEqualTo("W3C,B3"); + assertThat(environment.getProperty("server.shutdown")) + .as("Graceful shutdown must be on by default") + .isEqualTo("graceful"); + } + + @Test + void hostApplicationPropertiesOverrideFrameworkDefaults() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("management.endpoints.web.exposure.include", "health,info"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + // The user's existing property must still win — framework defaults are added last. + assertThat(environment.getProperty("management.endpoints.web.exposure.include")) + .isEqualTo("health,info"); + } + + @Test + void otelIsDefaultBridge() { + MockEnvironment environment = new MockEnvironment(); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + String excluded = environment.getProperty("spring.autoconfigure.exclude", ""); + assertThat(excluded) + .as("OTel is the default bridge, so the Brave auto-config must be excluded") + .contains("BraveAutoConfiguration"); + assertThat(excluded).doesNotContain("OpenTelemetryTracingAutoConfiguration"); + } + + @Test + void switchingToBraveExcludesOtel() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("firefly.observability.tracing.bridge", "BRAVE"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + String excluded = environment.getProperty("spring.autoconfigure.exclude", ""); + assertThat(excluded).contains("OpenTelemetryTracingAutoConfiguration"); + assertThat(excluded).contains("OtlpTracingAutoConfiguration"); + } + + @Test + void metricsOtlpFlippedWhenExporterIsOtlp() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("firefly.observability.metrics.exporter", "OTLP"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + assertThat(environment.getProperty("management.prometheus.metrics.export.enabled")).isEqualTo("false"); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("true"); + } + + @Test + void metricsBothFlipsAllOn() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("firefly.observability.metrics.exporter", "BOTH"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + assertThat(environment.getProperty("management.prometheus.metrics.export.enabled")).isEqualTo("true"); + assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("true"); + } + + @Test + void defaultsLoadingIsIdempotent() { + MockEnvironment environment = new MockEnvironment(); + MutablePropertySources sources = environment.getPropertySources(); + int sizeBefore = sources.size(); + + processor.postProcessEnvironment(environment, new SpringApplication()); + int sizeAfterFirst = sources.size(); + + processor.postProcessEnvironment(environment, new SpringApplication()); + int sizeAfterSecond = sources.size(); + + // First run adds defaults (and possibly the post-processor source). Second run should + // not duplicate the defaults source. + assertThat(sizeAfterSecond).isEqualTo(sizeAfterFirst); + assertThat(sizeAfterFirst).isGreaterThan(sizeBefore); + } +} From 4e06231435f0c772c6476993f2a06499f1e0a4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Contreras=20Guill=C3=A9n?= Date: Tue, 19 May 2026 17:57:07 +0200 Subject: [PATCH 2/2] release: bump version to 26.05.01 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e6345c1..c5bc87b 100644 --- a/pom.xml +++ b/pom.xml @@ -7,12 +7,12 @@ org.fireflyframework fireflyframework-parent - 26.04.01 + 26.05.01 fireflyframework-observability - 26.04.01 + 26.05.01 jar Firefly Framework - Observability