Skip to content

Commit e9d626b

Browse files
anconguiAndrés Contreras Guillén
andauthored
release: 26.05.01 — OTel observability foundation (#1)
* 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. * release: bump version to 26.05.01 --------- Co-authored-by: Andrés Contreras Guillén <ancongui@Andress-MacBook-Pro.local>
1 parent 2fb09a3 commit e9d626b

3 files changed

Lines changed: 174 additions & 4 deletions

File tree

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
<parent>
88
<groupId>org.fireflyframework</groupId>
99
<artifactId>fireflyframework-parent</artifactId>
10-
<version>26.04.01</version>
10+
<version>26.05.01</version>
1111
<relativePath/>
1212
</parent>
1313

1414
<artifactId>fireflyframework-observability</artifactId>
15-
<version>26.04.01</version>
15+
<version>26.05.01</version>
1616
<packaging>jar</packaging>
1717

1818
<name>Firefly Framework - Observability</name>

src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package org.fireflyframework.observability;
22

3+
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
34
import org.springframework.boot.SpringApplication;
45
import org.springframework.boot.env.EnvironmentPostProcessor;
56
import org.springframework.core.Ordered;
67
import org.springframework.core.env.ConfigurableEnvironment;
78
import org.springframework.core.env.MapPropertySource;
9+
import org.springframework.core.env.PropertiesPropertySource;
10+
import org.springframework.core.io.ClassPathResource;
11+
import org.springframework.core.io.Resource;
812

913
import java.util.LinkedHashMap;
1014
import java.util.Map;
15+
import java.util.Properties;
1116

1217
/**
1318
* Configures observability backends at startup based on Firefly properties.
@@ -56,6 +61,8 @@ public class FireflyObservabilityEnvironmentPostProcessor implements Environment
5661
private static final String OTLP_METRICS_ENABLED = "management.otlp.metrics.export.enabled";
5762

5863
private static final String PROPERTY_SOURCE_NAME = "fireflyObservabilityPostProcessor";
64+
private static final String DEFAULTS_SOURCE_NAME = "fireflyObservabilityDefaults";
65+
private static final String DEFAULTS_YAML = "application-firefly-observability.yml";
5966

6067
// Spring Boot actuator auto-configuration classes for each tracing bridge.
6168
// 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
7279

7380
@Override
7481
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
82+
// 1. Load framework default observability properties (actuator endpoints,
83+
// health groups, OTLP endpoints, etc.) — added with LOWEST precedence so the
84+
// user's own application.yml always wins.
85+
loadFrameworkDefaults(environment);
86+
87+
// 2. Resolve the active tracing bridge & metrics exporter and install the
88+
// corresponding Spring Boot auto-config exclusions / enable flags with
89+
// HIGHEST precedence.
7590
Map<String, Object> props = new LinkedHashMap<>();
76-
7791
configureTracingBridge(environment, props);
7892
configureMetricsExporter(environment, props);
79-
8093
if (!props.isEmpty()) {
8194
environment.getPropertySources().addFirst(
8295
new MapPropertySource(PROPERTY_SOURCE_NAME, props));
8396
}
8497
}
8598

99+
/**
100+
* Loads {@code application-firefly-observability.yml} from the classpath and adds
101+
* its contents to the environment with the lowest precedence so that any property
102+
* set by the host application overrides them.
103+
* <p>
104+
* If the resource is missing or unreadable, the method silently no-ops — the
105+
* framework still works, the host just won't get the default endpoint exposure,
106+
* tracing settings, OTLP endpoints, etc., until they opt in explicitly.
107+
*/
108+
private void loadFrameworkDefaults(ConfigurableEnvironment environment) {
109+
if (environment.getPropertySources().contains(DEFAULTS_SOURCE_NAME)) {
110+
return; // idempotent — never double-load if invoked twice
111+
}
112+
Resource resource = new ClassPathResource(DEFAULTS_YAML);
113+
if (!resource.exists()) {
114+
return;
115+
}
116+
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
117+
yaml.setResources(resource);
118+
Properties properties = yaml.getObject();
119+
if (properties == null || properties.isEmpty()) {
120+
return;
121+
}
122+
environment.getPropertySources().addLast(
123+
new PropertiesPropertySource(DEFAULTS_SOURCE_NAME, properties));
124+
}
125+
86126
private void configureTracingBridge(ConfigurableEnvironment environment, Map<String, Object> props) {
87127
boolean tracingEnabled = environment.getProperty(TRACING_ENABLED_PROPERTY, Boolean.class, true);
88128
if (!tracingEnabled) {
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2024-2026 Firefly Software Foundation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.fireflyframework.observability.autoconfigure;
18+
19+
import org.fireflyframework.observability.FireflyObservabilityEnvironmentPostProcessor;
20+
import org.junit.jupiter.api.Test;
21+
import org.springframework.boot.SpringApplication;
22+
import org.springframework.core.env.MutablePropertySources;
23+
import org.springframework.mock.env.MockEnvironment;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
class FireflyObservabilityEnvironmentPostProcessorTest {
28+
29+
private final FireflyObservabilityEnvironmentPostProcessor processor =
30+
new FireflyObservabilityEnvironmentPostProcessor();
31+
32+
@Test
33+
void defaultsAreLoadedFromYaml() {
34+
MockEnvironment environment = new MockEnvironment();
35+
36+
processor.postProcessEnvironment(environment, new SpringApplication());
37+
38+
// The bundled application-firefly-observability.yml should now be on the environment
39+
// so downstream services get sensible actuator/OTLP defaults out of the box.
40+
assertThat(environment.getProperty("management.endpoints.web.exposure.include"))
41+
.as("Default actuator endpoint exposure must be applied from framework defaults")
42+
.contains("prometheus");
43+
assertThat(environment.getProperty("management.endpoint.health.probes.enabled"))
44+
.as("Kubernetes liveness/readiness probes must be enabled by default")
45+
.isEqualTo("true");
46+
assertThat(environment.getProperty("management.tracing.propagation.type"))
47+
.as("Trace propagation default should be composite W3C+B3")
48+
.isEqualTo("W3C,B3");
49+
assertThat(environment.getProperty("server.shutdown"))
50+
.as("Graceful shutdown must be on by default")
51+
.isEqualTo("graceful");
52+
}
53+
54+
@Test
55+
void hostApplicationPropertiesOverrideFrameworkDefaults() {
56+
MockEnvironment environment = new MockEnvironment();
57+
environment.setProperty("management.endpoints.web.exposure.include", "health,info");
58+
59+
processor.postProcessEnvironment(environment, new SpringApplication());
60+
61+
// The user's existing property must still win — framework defaults are added last.
62+
assertThat(environment.getProperty("management.endpoints.web.exposure.include"))
63+
.isEqualTo("health,info");
64+
}
65+
66+
@Test
67+
void otelIsDefaultBridge() {
68+
MockEnvironment environment = new MockEnvironment();
69+
70+
processor.postProcessEnvironment(environment, new SpringApplication());
71+
72+
String excluded = environment.getProperty("spring.autoconfigure.exclude", "");
73+
assertThat(excluded)
74+
.as("OTel is the default bridge, so the Brave auto-config must be excluded")
75+
.contains("BraveAutoConfiguration");
76+
assertThat(excluded).doesNotContain("OpenTelemetryTracingAutoConfiguration");
77+
}
78+
79+
@Test
80+
void switchingToBraveExcludesOtel() {
81+
MockEnvironment environment = new MockEnvironment();
82+
environment.setProperty("firefly.observability.tracing.bridge", "BRAVE");
83+
84+
processor.postProcessEnvironment(environment, new SpringApplication());
85+
86+
String excluded = environment.getProperty("spring.autoconfigure.exclude", "");
87+
assertThat(excluded).contains("OpenTelemetryTracingAutoConfiguration");
88+
assertThat(excluded).contains("OtlpTracingAutoConfiguration");
89+
}
90+
91+
@Test
92+
void metricsOtlpFlippedWhenExporterIsOtlp() {
93+
MockEnvironment environment = new MockEnvironment();
94+
environment.setProperty("firefly.observability.metrics.exporter", "OTLP");
95+
96+
processor.postProcessEnvironment(environment, new SpringApplication());
97+
98+
assertThat(environment.getProperty("management.prometheus.metrics.export.enabled")).isEqualTo("false");
99+
assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("true");
100+
}
101+
102+
@Test
103+
void metricsBothFlipsAllOn() {
104+
MockEnvironment environment = new MockEnvironment();
105+
environment.setProperty("firefly.observability.metrics.exporter", "BOTH");
106+
107+
processor.postProcessEnvironment(environment, new SpringApplication());
108+
109+
assertThat(environment.getProperty("management.prometheus.metrics.export.enabled")).isEqualTo("true");
110+
assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("true");
111+
}
112+
113+
@Test
114+
void defaultsLoadingIsIdempotent() {
115+
MockEnvironment environment = new MockEnvironment();
116+
MutablePropertySources sources = environment.getPropertySources();
117+
int sizeBefore = sources.size();
118+
119+
processor.postProcessEnvironment(environment, new SpringApplication());
120+
int sizeAfterFirst = sources.size();
121+
122+
processor.postProcessEnvironment(environment, new SpringApplication());
123+
int sizeAfterSecond = sources.size();
124+
125+
// First run adds defaults (and possibly the post-processor source). Second run should
126+
// not duplicate the defaults source.
127+
assertThat(sizeAfterSecond).isEqualTo(sizeAfterFirst);
128+
assertThat(sizeAfterFirst).isGreaterThan(sizeBefore);
129+
}
130+
}

0 commit comments

Comments
 (0)