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);
+ }
+}