diff --git a/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java b/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java index 99948c6..2fa436d 100644 --- a/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java +++ b/src/main/java/org/fireflyframework/observability/FireflyObservabilityEnvironmentPostProcessor.java @@ -37,6 +37,13 @@ *
  • {@code OTLP} — disables Prometheus scrape, enables OTLP push
  • *
  • {@code BOTH} — enables both simultaneously
  • * + * + *

    OTLP Transport Translation

    + * Translates the OpenTelemetry {@code OTEL_EXPORTER_OTLP_PROTOCOL} env var + * ({@code grpc | http/protobuf | http/json}) into Spring's + * {@code management.otlp.tracing.transport} enum ({@code GRPC | HTTP}), moving the + * default endpoint to the HTTP port when HTTP is selected. See + * {@link #configureOtlpTracingTransport}. *

    * Runs at {@link Ordered#LOWEST_PRECEDENCE} to ensure {@code application.yml} * properties are already resolved. Existing user-defined @@ -59,6 +66,15 @@ public class FireflyObservabilityEnvironmentPostProcessor implements Environment private static final String EXCLUDE_PROPERTY = "spring.autoconfigure.exclude"; private static final String PROMETHEUS_ENABLED = "management.prometheus.metrics.export.enabled"; private static final String OTLP_METRICS_ENABLED = "management.otlp.metrics.export.enabled"; + private static final String OTLP_TRACING_TRANSPORT = "management.otlp.tracing.transport"; + private static final String OTLP_TRACING_ENDPOINT = "management.otlp.tracing.endpoint"; + + // --- OpenTelemetry SDK environment variables --- + private static final String OTEL_PROTOCOL_ENV = "OTEL_EXPORTER_OTLP_PROTOCOL"; + private static final String OTEL_TRACES_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; + private static final String OTEL_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT"; + // Default OTLP/HTTP traces endpoint (gRPC lives on :4317, HTTP on :4318 with the /v1/traces path). + private static final String DEFAULT_HTTP_TRACES_ENDPOINT = "http://localhost:4318/v1/traces"; private static final String PROPERTY_SOURCE_NAME = "fireflyObservabilityPostProcessor"; private static final String DEFAULTS_SOURCE_NAME = "fireflyObservabilityDefaults"; @@ -90,6 +106,7 @@ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringAp Map props = new LinkedHashMap<>(); configureTracingBridge(environment, props); configureMetricsExporter(environment, props); + configureOtlpTracingTransport(environment, props); if (!props.isEmpty()) { environment.getPropertySources().addFirst( new MapPropertySource(PROPERTY_SOURCE_NAME, props)); @@ -175,6 +192,45 @@ private void configureMetricsExporter(ConfigurableEnvironment environment, Map + * The OTel spec values are {@code grpc | http/protobuf | http/json}, but Spring's + * {@code Transport} enum only accepts {@code GRPC | HTTP}. Binding the raw spec value + * straight to the enum (as the bundled YAML previously did) fails to bind + * {@code http/protobuf} and crashes context initialization. We normalize any + * {@code http/*} protocol to {@code http} here. + *

    + * gRPC and HTTP also listen on different ports/paths ({@code :4317} vs + * {@code :4318/v1/traces}). When HTTP is selected without an explicit endpoint, we move + * the default off the gRPC port so the exporter does not POST to a gRPC listener at + * runtime. An explicit {@code OTEL_EXPORTER_OTLP_(TRACES_)ENDPOINT} always wins. + *

    + * No-ops when the protocol variable is unset, leaving the YAML {@code grpc} default in place. + */ + private void configureOtlpTracingTransport(ConfigurableEnvironment environment, Map props) { + String protocol = environment.getProperty(OTEL_PROTOCOL_ENV); + if (protocol == null || protocol.isBlank()) { + return; + } + boolean http = protocol.trim().toLowerCase().startsWith("http"); + props.put(OTLP_TRACING_TRANSPORT, http ? "http" : "grpc"); + + if (http && !hasExplicitOtlpEndpoint(environment)) { + props.put(OTLP_TRACING_ENDPOINT, DEFAULT_HTTP_TRACES_ENDPOINT); + } + } + + private boolean hasExplicitOtlpEndpoint(ConfigurableEnvironment environment) { + return isSet(environment.getProperty(OTEL_TRACES_ENDPOINT_ENV)) + || isSet(environment.getProperty(OTEL_ENDPOINT_ENV)); + } + + private static boolean isSet(String value) { + return value != null && !value.isBlank(); + } + /** * Runs after {@code ConfigDataEnvironmentPostProcessor} so that * {@code application.yml} properties are available when we read the settings. diff --git a/src/main/resources/application-firefly-observability.yml b/src/main/resources/application-firefly-observability.yml index c7f3032..2a712bf 100644 --- a/src/main/resources/application-firefly-observability.yml +++ b/src/main/resources/application-firefly-observability.yml @@ -87,7 +87,10 @@ management: otlp: tracing: endpoint: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317}} - transport: ${OTEL_EXPORTER_OTLP_PROTOCOL:grpc} + # grpc default. OTEL_EXPORTER_OTLP_PROTOCOL (grpc|http/protobuf|http/json) is translated to + # this enum (GRPC|HTTP) by FireflyObservabilityEnvironmentPostProcessor — binding the raw + # spec value here would fail to bind http/protobuf and crash context init. + transport: grpc metrics: export: enabled: false # Overridden by EnvironmentPostProcessor based on exporter property diff --git a/src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java b/src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java index 276102d..45fb847 100644 --- a/src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java +++ b/src/test/java/org/fireflyframework/observability/autoconfigure/FireflyObservabilityEnvironmentPostProcessorTest.java @@ -110,6 +110,70 @@ void metricsBothFlipsAllOn() { assertThat(environment.getProperty("management.otlp.metrics.export.enabled")).isEqualTo("true"); } + @Test + void otlpProtocolHttpProtobufTranslatesToHttpTransport() { + MockEnvironment environment = new MockEnvironment(); + // The OTel-spec value that previously crashed context init by binding straight to the enum. + environment.setProperty("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + assertThat(environment.getProperty("management.otlp.tracing.transport")) + .as("http/protobuf must map to the HTTP transport enum, not the raw spec value") + .isEqualTo("http"); + assertThat(environment.getProperty("management.otlp.tracing.endpoint")) + .as("HTTP transport must move off the gRPC :4317 port to the :4318 /v1/traces path") + .isEqualTo("http://localhost:4318/v1/traces"); + } + + @Test + void otlpProtocolHttpJsonAlsoTranslatesToHttpTransport() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("OTEL_EXPORTER_OTLP_PROTOCOL", "http/json"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + assertThat(environment.getProperty("management.otlp.tracing.transport")).isEqualTo("http"); + } + + @Test + void otlpProtocolGrpcKeepsGrpcTransportAndPort() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + assertThat(environment.getProperty("management.otlp.tracing.transport")).isEqualTo("grpc"); + assertThat(environment.getProperty("management.otlp.tracing.endpoint")) + .as("gRPC must keep the default :4317 endpoint") + .isEqualTo("http://localhost:4317"); + } + + @Test + void otlpProtocolUnsetLeavesGrpcDefault() { + MockEnvironment environment = new MockEnvironment(); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + assertThat(environment.getProperty("management.otlp.tracing.transport")) + .as("No OTel protocol override means the YAML grpc default stands") + .isEqualTo("grpc"); + } + + @Test + void explicitOtlpEndpointIsRespectedWhenSwitchingToHttp() { + MockEnvironment environment = new MockEnvironment(); + environment.setProperty("OTEL_EXPORTER_OTLP_PROTOCOL", "http/protobuf"); + environment.setProperty("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector.monitoring:4318"); + + processor.postProcessEnvironment(environment, new SpringApplication()); + + assertThat(environment.getProperty("management.otlp.tracing.transport")).isEqualTo("http"); + assertThat(environment.getProperty("management.otlp.tracing.endpoint")) + .as("An explicit endpoint must win over the derived HTTP default") + .isEqualTo("http://collector.monitoring:4318"); + } + @Test void defaultsLoadingIsIdempotent() { MockEnvironment environment = new MockEnvironment();