Create spring-boot-tracing module

This commit is contained in:
Andy Wilkinson
2025-06-06 14:27:18 +01:00
committed by Phillip Webb
parent f680582019
commit fb886a1818
90 changed files with 240 additions and 277 deletions

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import brave.CurrentSpanCustomizer;
import brave.SpanCustomizer;
import brave.Tracer;
import brave.Tracing;
import brave.Tracing.Builder;
import brave.TracingCustomizer;
import brave.handler.SpanHandler;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContextCustomizer;
import brave.propagation.Propagation.Factory;
import brave.propagation.ThreadLocalCurrentTraceContext;
import brave.sampler.Sampler;
import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
import io.micrometer.tracing.brave.bridge.BraveCurrentTraceContext;
import io.micrometer.tracing.brave.bridge.BravePropagator;
import io.micrometer.tracing.brave.bridge.BraveSpanCustomizer;
import io.micrometer.tracing.brave.bridge.BraveTracer;
import io.micrometer.tracing.brave.bridge.CompositeSpanHandler;
import io.micrometer.tracing.exporter.SpanExportingPredicate;
import io.micrometer.tracing.exporter.SpanFilter;
import io.micrometer.tracing.exporter.SpanReporter;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.IncompatibleConfigurationException;
import org.springframework.boot.tracing.autoconfigure.TracingProperties.Propagation.PropagationType;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Brave.
*
* @author Moritz Halbritter
* @author Marcin Grzejszczak
* @author Jonatan Ivanov
* @since 4.0.0
*/
@AutoConfiguration(before = { MicrometerTracingAutoConfiguration.class, NoopTracerAutoConfiguration.class })
@ConditionalOnClass({ Tracer.class, BraveTracer.class })
@EnableConfigurationProperties(TracingProperties.class)
@Import({ BravePropagationConfigurations.PropagationWithoutBaggage.class,
BravePropagationConfigurations.PropagationWithBaggage.class,
BravePropagationConfigurations.NoPropagation.class })
public class BraveAutoConfiguration {
/**
* Default value for application name if {@code spring.application.name} is not set.
*/
private static final String DEFAULT_APPLICATION_NAME = "application";
private final TracingProperties tracingProperties;
BraveAutoConfiguration(TracingProperties tracingProperties) {
this.tracingProperties = tracingProperties;
}
@Bean
@ConditionalOnMissingBean
@Order(Ordered.HIGHEST_PRECEDENCE)
CompositeSpanHandler compositeSpanHandler(ObjectProvider<SpanExportingPredicate> predicates,
ObjectProvider<SpanReporter> reporters, ObjectProvider<SpanFilter> filters) {
return new CompositeSpanHandler(predicates.orderedStream().toList(), reporters.orderedStream().toList(),
filters.orderedStream().toList());
}
@Bean
@ConditionalOnMissingBean
Tracing braveTracing(Environment environment, List<SpanHandler> spanHandlers,
List<TracingCustomizer> tracingCustomizers, CurrentTraceContext currentTraceContext,
Factory propagationFactory, Sampler sampler) {
if (this.tracingProperties.getBrave().isSpanJoiningSupported()) {
if (this.tracingProperties.getPropagation().getType() != null
&& this.tracingProperties.getPropagation().getType().contains(PropagationType.W3C)) {
throw new IncompatibleConfigurationException("management.tracing.propagation.type",
"management.tracing.brave.span-joining-supported");
}
if (this.tracingProperties.getPropagation().getType() == null
&& this.tracingProperties.getPropagation().getProduce().contains(PropagationType.W3C)) {
throw new IncompatibleConfigurationException("management.tracing.propagation.produce",
"management.tracing.brave.span-joining-supported");
}
if (this.tracingProperties.getPropagation().getType() == null
&& this.tracingProperties.getPropagation().getConsume().contains(PropagationType.W3C)) {
throw new IncompatibleConfigurationException("management.tracing.propagation.consume",
"management.tracing.brave.span-joining-supported");
}
}
String applicationName = environment.getProperty("spring.application.name", DEFAULT_APPLICATION_NAME);
Builder builder = Tracing.newBuilder()
.currentTraceContext(currentTraceContext)
.traceId128Bit(true)
.supportsJoin(this.tracingProperties.getBrave().isSpanJoiningSupported())
.propagationFactory(propagationFactory)
.sampler(sampler)
.localServiceName(applicationName);
spanHandlers.forEach(builder::addSpanHandler);
for (TracingCustomizer tracingCustomizer : tracingCustomizers) {
tracingCustomizer.customize(builder);
}
return builder.build();
}
@Bean
@ConditionalOnMissingBean
brave.Tracer braveTracer(Tracing tracing) {
return tracing.tracer();
}
@Bean
@ConditionalOnMissingBean
CurrentTraceContext braveCurrentTraceContext(List<CurrentTraceContext.ScopeDecorator> scopeDecorators,
List<CurrentTraceContextCustomizer> currentTraceContextCustomizers) {
ThreadLocalCurrentTraceContext.Builder builder = ThreadLocalCurrentTraceContext.newBuilder();
scopeDecorators.forEach(builder::addScopeDecorator);
for (CurrentTraceContextCustomizer currentTraceContextCustomizer : currentTraceContextCustomizers) {
currentTraceContextCustomizer.customize(builder);
}
return builder.build();
}
@Bean
@ConditionalOnMissingBean
Sampler braveSampler() {
return Sampler.create(this.tracingProperties.getSampling().getProbability());
}
@Bean
@ConditionalOnMissingBean(io.micrometer.tracing.Tracer.class)
BraveTracer braveTracerBridge(brave.Tracer tracer, CurrentTraceContext currentTraceContext) {
return new BraveTracer(tracer, new BraveCurrentTraceContext(currentTraceContext),
new BraveBaggageManager(this.tracingProperties.getBaggage().getTagFields(),
this.tracingProperties.getBaggage().getRemoteFields()));
}
@Bean
@ConditionalOnMissingBean
BravePropagator bravePropagator(Tracing tracing) {
return new BravePropagator(tracing);
}
@Bean
@ConditionalOnMissingBean(SpanCustomizer.class)
CurrentSpanCustomizer currentSpanCustomizer(Tracing tracing) {
return CurrentSpanCustomizer.create(tracing);
}
@Bean
@ConditionalOnMissingBean(io.micrometer.tracing.SpanCustomizer.class)
BraveSpanCustomizer braveSpanCustomizer(SpanCustomizer spanCustomizer) {
return new BraveSpanCustomizer(spanCustomizer);
}
}

View File

@@ -0,0 +1,181 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import brave.baggage.BaggageField;
import brave.baggage.BaggagePropagation;
import brave.baggage.BaggagePropagation.FactoryBuilder;
import brave.baggage.BaggagePropagationConfig;
import brave.baggage.BaggagePropagationCustomizer;
import brave.baggage.CorrelationScopeConfig.SingleCorrelationField;
import brave.baggage.CorrelationScopeCustomizer;
import brave.baggage.CorrelationScopeDecorator;
import brave.context.slf4j.MDCScopeDecorator;
import brave.propagation.CurrentTraceContext.ScopeDecorator;
import brave.propagation.Propagation;
import brave.propagation.Propagation.Factory;
import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.tracing.autoconfigure.TracingProperties.Baggage.Correlation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
/**
* Brave propagation configurations. They are imported by {@link BraveAutoConfiguration}.
*
* @author Moritz Halbritter
*/
class BravePropagationConfigurations {
/**
* Propagates traces but no baggage.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty(name = "management.tracing.baggage.enabled", havingValue = false)
static class PropagationWithoutBaggage {
@Bean
@ConditionalOnMissingBean(Factory.class)
@ConditionalOnEnabledTracing
CompositePropagationFactory propagationFactory(TracingProperties properties) {
return CompositePropagationFactory.create(properties.getPropagation());
}
}
/**
* Propagates traces and baggage.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty(name = "management.tracing.baggage.enabled", matchIfMissing = true)
@EnableConfigurationProperties(TracingProperties.class)
static class PropagationWithBaggage {
private final TracingProperties tracingProperties;
PropagationWithBaggage(TracingProperties tracingProperties) {
this.tracingProperties = tracingProperties;
}
@Bean
@ConditionalOnMissingBean
BaggagePropagation.FactoryBuilder propagationFactoryBuilder(
ObjectProvider<BaggagePropagationCustomizer> baggagePropagationCustomizers) {
// There's a chicken-and-egg problem here: to create a builder, we need a
// factory. But the CompositePropagationFactory needs data from the builder.
// We create a throw-away builder with a throw-away factory, and then copy the
// config to the real builder.
FactoryBuilder throwAwayBuilder = BaggagePropagation.newFactoryBuilder(createThrowAwayFactory());
baggagePropagationCustomizers.orderedStream()
.forEach((customizer) -> customizer.customize(throwAwayBuilder));
CompositePropagationFactory propagationFactory = CompositePropagationFactory.create(
this.tracingProperties.getPropagation(),
new BraveBaggageManager(this.tracingProperties.getBaggage().getTagFields(),
this.tracingProperties.getBaggage().getRemoteFields()),
LocalBaggageFields.extractFrom(throwAwayBuilder));
FactoryBuilder builder = BaggagePropagation.newFactoryBuilder(propagationFactory);
throwAwayBuilder.configs().forEach(builder::add);
return builder;
}
private Factory createThrowAwayFactory() {
return new Factory() {
@Override
public Propagation<String> get() {
return null;
}
};
}
@Bean
BaggagePropagationCustomizer remoteFieldsBaggagePropagationCustomizer() {
return (builder) -> {
List<String> remoteFields = this.tracingProperties.getBaggage().getRemoteFields();
for (String fieldName : remoteFields) {
builder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create(fieldName)));
}
List<String> localFields = this.tracingProperties.getBaggage().getLocalFields();
for (String localFieldName : localFields) {
builder.add(BaggagePropagationConfig.SingleBaggageField.local(BaggageField.create(localFieldName)));
}
};
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledTracing
Factory propagationFactory(BaggagePropagation.FactoryBuilder factoryBuilder) {
return factoryBuilder.build();
}
@Bean
@ConditionalOnMissingBean
CorrelationScopeDecorator.Builder mdcCorrelationScopeDecoratorBuilder(
ObjectProvider<CorrelationScopeCustomizer> correlationScopeCustomizers) {
CorrelationScopeDecorator.Builder builder = MDCScopeDecorator.newBuilder();
correlationScopeCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
}
@Bean
@Order(0)
@ConditionalOnBooleanProperty(name = "management.tracing.baggage.correlation.enabled", matchIfMissing = true)
CorrelationScopeCustomizer correlationFieldsCorrelationScopeCustomizer() {
return (builder) -> {
Correlation correlationProperties = this.tracingProperties.getBaggage().getCorrelation();
for (String field : correlationProperties.getFields()) {
BaggageField baggageField = BaggageField.create(field);
SingleCorrelationField correlationField = SingleCorrelationField.newBuilder(baggageField)
.flushOnUpdate()
.build();
builder.add(correlationField);
}
};
}
@Bean
@ConditionalOnMissingBean(CorrelationScopeDecorator.class)
ScopeDecorator correlationScopeDecorator(CorrelationScopeDecorator.Builder builder) {
return builder.build();
}
}
/**
* Propagates neither traces nor baggage.
*/
@Configuration(proxyBeanMethods = false)
static class NoPropagation {
@Bean
@ConditionalOnMissingBean(Factory.class)
CompositePropagationFactory noopPropagationFactory() {
return CompositePropagationFactory.noop();
}
}
}

View File

@@ -0,0 +1,244 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Stream;
import brave.propagation.B3Propagation;
import brave.propagation.Propagation;
import brave.propagation.Propagation.Factory;
import brave.propagation.TraceContext;
import brave.propagation.TraceContextOrSamplingFlags;
import io.micrometer.tracing.BaggageManager;
import io.micrometer.tracing.brave.bridge.W3CPropagation;
import org.springframework.boot.tracing.autoconfigure.TracingProperties.Propagation.PropagationType;
/**
* {@link brave.propagation.Propagation.Factory Propagation factory} which supports
* multiple tracing formats. It is able to configure different formats for injecting and
* for extracting.
*
* @author Marcin Grzejszczak
* @author Moritz Halbritter
* @author Phillip Webb
*/
class CompositePropagationFactory extends Propagation.Factory {
private final PropagationFactories injectors;
private final PropagationFactories extractors;
private final CompositePropagation propagation;
CompositePropagationFactory(Collection<Factory> injectorFactories, Collection<Factory> extractorFactories) {
this.injectors = new PropagationFactories(injectorFactories);
this.extractors = new PropagationFactories(extractorFactories);
this.propagation = new CompositePropagation(this.injectors, this.extractors);
}
Stream<Factory> getInjectors() {
return this.injectors.stream();
}
@Override
public boolean supportsJoin() {
return this.injectors.supportsJoin() && this.extractors.supportsJoin();
}
@Override
public boolean requires128BitTraceId() {
return this.injectors.requires128BitTraceId() || this.extractors.requires128BitTraceId();
}
@Override
public Propagation<String> get() {
return this.propagation;
}
@Override
public TraceContext decorate(TraceContext context) {
return Stream.concat(this.injectors.stream(), this.extractors.stream())
.map((factory) -> factory.decorate(context))
.filter((decorated) -> decorated != context)
.findFirst()
.orElse(context);
}
/**
* Creates a new {@link CompositePropagationFactory} which doesn't do any propagation.
* @return the {@link CompositePropagationFactory}
*/
static CompositePropagationFactory noop() {
return new CompositePropagationFactory(Collections.emptyList(), Collections.emptyList());
}
/**
* Creates a new {@link CompositePropagationFactory}.
* @param properties the propagation properties
* @return the {@link CompositePropagationFactory}
*/
static CompositePropagationFactory create(TracingProperties.Propagation properties) {
return create(properties, null, null);
}
/**
* Creates a new {@link CompositePropagationFactory}.
* @param properties the propagation properties
* @param baggageManager the baggage manager to use, or {@code null}
* @param localFields the local fields, or {@code null}
* @return the {@link CompositePropagationFactory}
*/
static CompositePropagationFactory create(TracingProperties.Propagation properties, BaggageManager baggageManager,
LocalBaggageFields localFields) {
PropagationFactoryMapper mapper = new PropagationFactoryMapper(baggageManager, localFields);
List<Factory> injectors = properties.getEffectiveProducedTypes().stream().map(mapper::map).toList();
List<Factory> extractors = properties.getEffectiveConsumedTypes().stream().map(mapper::map).toList();
return new CompositePropagationFactory(injectors, extractors);
}
/**
* Mapper used to create a {@link brave.propagation.Propagation.Factory Propagation
* factory} from a {@link PropagationType}.
*/
private static class PropagationFactoryMapper {
private final BaggageManager baggageManager;
private final LocalBaggageFields localFields;
PropagationFactoryMapper(BaggageManager baggageManager, LocalBaggageFields localFields) {
this.baggageManager = baggageManager;
this.localFields = (localFields != null) ? localFields : LocalBaggageFields.empty();
}
Propagation.Factory map(PropagationType type) {
return switch (type) {
case B3 -> b3Single();
case B3_MULTI -> b3Multi();
case W3C -> w3c();
};
}
/**
* Creates a new B3 propagation factory using a single B3 header.
* @return the B3 propagation factory
*/
private Propagation.Factory b3Single() {
return B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.SINGLE).build();
}
/**
* Creates a new B3 propagation factory using multiple B3 headers.
* @return the B3 propagation factory
*/
private Propagation.Factory b3Multi() {
return B3Propagation.newFactoryBuilder().injectFormat(B3Propagation.Format.MULTI).build();
}
/**
* Creates a new W3C propagation factory.
* @return the W3C propagation factory
*/
private Propagation.Factory w3c() {
if (this.baggageManager == null) {
return new W3CPropagation();
}
return new W3CPropagation(this.baggageManager, this.localFields.asList());
}
}
/**
* A collection of propagation factories.
*/
private static class PropagationFactories {
private final List<Propagation.Factory> factories;
PropagationFactories(Collection<Factory> factories) {
this.factories = List.copyOf(factories);
}
boolean requires128BitTraceId() {
return stream().anyMatch(Propagation.Factory::requires128BitTraceId);
}
boolean supportsJoin() {
return stream().allMatch(Propagation.Factory::supportsJoin);
}
List<Propagation<String>> get() {
return stream().map(Factory::get).toList();
}
Stream<Factory> stream() {
return this.factories.stream();
}
}
/**
* A composite {@link Propagation}.
*/
private static class CompositePropagation implements Propagation<String> {
private final List<Propagation<String>> injectors;
private final List<Propagation<String>> extractors;
private final List<String> keys;
CompositePropagation(PropagationFactories injectorFactories, PropagationFactories extractorFactories) {
this.injectors = injectorFactories.get();
this.extractors = extractorFactories.get();
this.keys = Stream.concat(keys(this.injectors), keys(this.extractors)).distinct().toList();
}
private Stream<String> keys(List<Propagation<String>> propagations) {
return propagations.stream().flatMap((propagation) -> propagation.keys().stream());
}
@Override
public List<String> keys() {
return this.keys;
}
@Override
public <R> TraceContext.Injector<R> injector(Setter<R, String> setter) {
return (traceContext, request) -> this.injectors.stream()
.map((propagation) -> propagation.injector(setter))
.forEach((injector) -> injector.inject(traceContext, request));
}
@Override
public <R> TraceContext.Extractor<R> extractor(Getter<R, String> getter) {
return (request) -> this.extractors.stream()
.map((propagation) -> propagation.extractor(getter))
.map((extractor) -> extractor.extract(request))
.filter(Predicate.not(TraceContextOrSamplingFlags.EMPTY::equals))
.findFirst()
.orElse(TraceContextOrSamplingFlags.EMPTY);
}
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import org.springframework.boot.tracing.autoconfigure.TracingProperties.Propagation.PropagationType;
/**
* {@link TextMapPropagator} which supports multiple tracing formats. It is able to
* configure different formats for injecting and for extracting.
*
* @author Moritz Halbritter
* @author Scott Frederick
*/
class CompositeTextMapPropagator implements TextMapPropagator {
private final Collection<TextMapPropagator> injectors;
private final Collection<TextMapPropagator> extractors;
private final TextMapPropagator baggagePropagator;
private final Set<String> fields;
/**
* Creates a new {@link CompositeTextMapPropagator}.
* @param injectors the injectors
* @param mutuallyExclusiveExtractors the mutually exclusive extractors. They are
* applied in order, and as soon as an extractor extracts a context, the other
* extractors after it are no longer invoked
* @param baggagePropagator the baggage propagator to use, or {@code null}
*/
CompositeTextMapPropagator(Collection<TextMapPropagator> injectors,
Collection<TextMapPropagator> mutuallyExclusiveExtractors, TextMapPropagator baggagePropagator) {
this.injectors = injectors;
this.extractors = mutuallyExclusiveExtractors;
this.baggagePropagator = baggagePropagator;
Set<String> fields = new LinkedHashSet<>();
fields(this.injectors).forEach(fields::add);
fields(this.extractors).forEach(fields::add);
if (baggagePropagator != null) {
fields.addAll(baggagePropagator.fields());
}
this.fields = Collections.unmodifiableSet(fields);
}
private Stream<String> fields(Collection<TextMapPropagator> propagators) {
return propagators.stream().flatMap((propagator) -> propagator.fields().stream());
}
Collection<TextMapPropagator> getInjectors() {
return this.injectors;
}
Collection<TextMapPropagator> getExtractors() {
return this.extractors;
}
@Override
public Collection<String> fields() {
return this.fields;
}
@Override
public <C> void inject(Context context, C carrier, TextMapSetter<C> setter) {
if (context != null && setter != null) {
this.injectors.forEach((injector) -> injector.inject(context, carrier, setter));
}
}
@Override
public <C> Context extract(Context context, C carrier, TextMapGetter<C> getter) {
if (context == null) {
return Context.root();
}
if (getter == null) {
return context;
}
Context result = this.extractors.stream()
.map((extractor) -> extractor.extract(context, carrier, getter))
.filter((extracted) -> extracted != context)
.findFirst()
.orElse(context);
if (this.baggagePropagator != null) {
result = this.baggagePropagator.extract(result, carrier, getter);
}
return result;
}
/**
* Creates a new {@link CompositeTextMapPropagator}.
* @param properties the tracing properties
* @param baggagePropagator the baggage propagator to use, or {@code null}
* @return the {@link CompositeTextMapPropagator}
*/
static TextMapPropagator create(TracingProperties.Propagation properties, TextMapPropagator baggagePropagator) {
TextMapPropagatorMapper mapper = new TextMapPropagatorMapper(baggagePropagator != null);
List<TextMapPropagator> injectors = properties.getEffectiveProducedTypes()
.stream()
.map(mapper::map)
.collect(Collectors.toCollection(ArrayList::new));
if (baggagePropagator != null) {
injectors.add(baggagePropagator);
}
List<TextMapPropagator> extractors = properties.getEffectiveConsumedTypes().stream().map(mapper::map).toList();
return new CompositeTextMapPropagator(injectors, extractors, baggagePropagator);
}
/**
* Mapper used to create a {@link TextMapPropagator} from a {@link PropagationType}.
*/
private static class TextMapPropagatorMapper {
private final boolean baggage;
TextMapPropagatorMapper(boolean baggage) {
this.baggage = baggage;
}
TextMapPropagator map(PropagationType type) {
return switch (type) {
case B3 -> b3Single();
case B3_MULTI -> b3Multi();
case W3C -> w3c();
};
}
/**
* Creates a new B3 propagator using a single B3 header.
* @return the B3 propagator
*/
private TextMapPropagator b3Single() {
return B3Propagator.injectingSingleHeader();
}
/**
* Creates a new B3 propagator using multiple B3 headers.
* @return the B3 propagator
*/
private TextMapPropagator b3Multi() {
return B3Propagator.injectingMultiHeaders();
}
/**
* Creates a new W3C propagator.
* @return the W3C propagator
*/
private TextMapPropagator w3c() {
return (!this.baggage) ? W3CTraceContextPropagator.getInstance() : TextMapPropagator
.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance());
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
/**
* {@link Conditional @Conditional} that checks whether tracing is enabled. It matches if
* the value of the {@code management.tracing.enabled} property is {@code true} or if it
* is not configured. If the {@link #value() tracing exporter name} is set, the
* {@code management.<name>.tracing.export.enabled} property can be used to control the
* behavior for the specific tracing exporter. In that case, the exporter specific
* property takes precedence over the global property.
*
* @author Moritz Halbritter
* @since 4.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnEnabledTracingCondition.class)
public @interface ConditionalOnEnabledTracing {
/**
* Name of the tracing exporter.
* @return the name of the tracing exporter
* @since 3.4.0
*/
String value() default "";
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import brave.baggage.BaggagePropagation;
import brave.baggage.BaggagePropagationConfig;
import brave.baggage.BaggagePropagationConfig.SingleBaggageField;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Local baggage fields.
*
* @author Moritz Halbritter
*/
class LocalBaggageFields {
private final List<String> fields;
LocalBaggageFields(List<String> fields) {
Assert.notNull(fields, "'fields' must not be null");
this.fields = fields;
}
/**
* Returns the local fields as a list.
* @return the list
*/
List<String> asList() {
return Collections.unmodifiableList(this.fields);
}
/**
* Extracts the local fields from the given propagation factory builder.
* @param builder the propagation factory builder to extract the local fields from
* @return the local fields
*/
static LocalBaggageFields extractFrom(BaggagePropagation.FactoryBuilder builder) {
List<String> localFields = new ArrayList<>();
for (BaggagePropagationConfig config : builder.configs()) {
if (config instanceof SingleBaggageField field) {
if (CollectionUtils.isEmpty(field.keyNames())) {
localFields.add(field.field().name());
}
}
}
return new LocalBaggageFields(localFields);
}
/**
* Creates empty local fields.
* @return the empty local fields
*/
static LocalBaggageFields empty() {
return new LocalBaggageFields(Collections.emptyList());
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
/**
* {@link EnvironmentPostProcessor} to add a {@link PropertySource} to support log
* correlation IDs when Micrometer Tracing is present. Adds support for the
* {@value LoggingSystem#EXPECT_CORRELATION_ID_PROPERTY} property by delegating to
* {@code management.tracing.enabled}.
*
* @author Jonatan Ivanov
* @author Phillip Webb
*/
class LogCorrelationEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (ClassUtils.isPresent("io.micrometer.tracing.Tracer", application.getClassLoader())) {
environment.getPropertySources().addLast(new LogCorrelationPropertySource(this, environment));
}
}
/**
* Log correlation {@link PropertySource}.
*/
private static class LogCorrelationPropertySource extends EnumerablePropertySource<Object> {
private static final String NAME = "logCorrelation";
private final Environment environment;
LogCorrelationPropertySource(Object source, Environment environment) {
super(NAME, source);
this.environment = environment;
}
@Override
public String[] getPropertyNames() {
return new String[] { LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY };
}
@Override
public Object getProperty(String name) {
if (name.equals(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY)) {
return this.environment.getProperty("management.tracing.enabled", Boolean.class, Boolean.TRUE);
}
return null;
}
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.annotation.DefaultNewSpanParser;
import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor;
import io.micrometer.tracing.annotation.MethodInvocationProcessor;
import io.micrometer.tracing.annotation.NewSpanParser;
import io.micrometer.tracing.annotation.SpanAspect;
import io.micrometer.tracing.annotation.SpanTagAnnotationHandler;
import io.micrometer.tracing.handler.DefaultTracingObservationHandler;
import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler;
import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler;
import io.micrometer.tracing.handler.TracingObservationHandler;
import io.micrometer.tracing.propagation.Propagator;
import org.aspectj.weaver.Advice;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.util.ClassUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for the Micrometer Tracing API.
*
* @author Moritz Halbritter
* @author Jonatan Ivanov
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnBean(Tracer.class)
public class MicrometerTracingAutoConfiguration {
/**
* {@code @Order} value of {@link #defaultTracingObservationHandler(Tracer)}.
*/
public static final int DEFAULT_TRACING_OBSERVATION_HANDLER_ORDER = Ordered.LOWEST_PRECEDENCE - 1000;
/**
* {@code @Order} value of
* {@link #propagatingReceiverTracingObservationHandler(Tracer, Propagator)}.
*/
public static final int RECEIVER_TRACING_OBSERVATION_HANDLER_ORDER = 1000;
/**
* {@code @Order} value of
* {@link #propagatingSenderTracingObservationHandler(Tracer, Propagator)}.
*/
public static final int SENDER_TRACING_OBSERVATION_HANDLER_ORDER = 2000;
@Bean
public ObservationHandlerGroup tracingObservationHandlerGroup(Tracer tracer) {
return ClassUtils.isPresent("io.micrometer.core.instrument.MeterRegistry", null)
? new TracingAndMeterObservationHandlerGroup(tracer)
: ObservationHandlerGroup.of(TracingObservationHandler.class);
}
@Bean
@ConditionalOnMissingBean
@Order(DEFAULT_TRACING_OBSERVATION_HANDLER_ORDER)
public DefaultTracingObservationHandler defaultTracingObservationHandler(Tracer tracer) {
return new DefaultTracingObservationHandler(tracer);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(Propagator.class)
@Order(SENDER_TRACING_OBSERVATION_HANDLER_ORDER)
public PropagatingSenderTracingObservationHandler<?> propagatingSenderTracingObservationHandler(Tracer tracer,
Propagator propagator) {
return new PropagatingSenderTracingObservationHandler<>(tracer, propagator);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(Propagator.class)
@Order(RECEIVER_TRACING_OBSERVATION_HANDLER_ORDER)
public PropagatingReceiverTracingObservationHandler<?> propagatingReceiverTracingObservationHandler(Tracer tracer,
Propagator propagator) {
return new PropagatingReceiverTracingObservationHandler<>(tracer, propagator);
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
@ConditionalOnBooleanProperty("management.observations.annotations.enabled")
static class SpanAspectConfiguration {
@Bean
@ConditionalOnMissingBean(NewSpanParser.class)
DefaultNewSpanParser newSpanParser() {
return new DefaultNewSpanParser();
}
@Bean
@ConditionalOnMissingBean
SpanTagAnnotationHandler spanTagAnnotationHandler(BeanFactory beanFactory) {
ValueExpressionResolver valueExpressionResolver = new SpelTagValueExpressionResolver();
return new SpanTagAnnotationHandler(beanFactory::getBean, (ignored) -> valueExpressionResolver);
}
@Bean
@ConditionalOnMissingBean(MethodInvocationProcessor.class)
ImperativeMethodInvocationProcessor imperativeMethodInvocationProcessor(NewSpanParser newSpanParser,
Tracer tracer, SpanTagAnnotationHandler spanTagAnnotationHandler) {
return new ImperativeMethodInvocationProcessor(newSpanParser, tracer, spanTagAnnotationHandler);
}
@Bean
@ConditionalOnMissingBean
SpanAspect spanAspect(MethodInvocationProcessor methodInvocationProcessor) {
return new SpanAspect(methodInvocationProcessor);
}
}
private static final class SpelTagValueExpressionResolver implements ValueExpressionResolver {
@Override
public String resolve(String expression, Object parameter) {
try {
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expressionToEvaluate = expressionParser.parseExpression(expression);
return expressionToEvaluate.getValue(context, parameter, String.class);
}
catch (Exception ex) {
throw new IllegalStateException("Unable to evaluate SpEL expression '%s'".formatted(expression), ex);
}
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import io.micrometer.tracing.Tracer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
/**
* {@link EnableAutoConfiguration Auto-configuration} for a no-op implementation of
* {@link Tracer}.
*
* @author Moritz Halbritter
* @since 4.0.0
*/
@AutoConfiguration(before = MicrometerTracingAutoConfiguration.class)
@ConditionalOnClass(Tracer.class)
@ConditionalOnMissingBean(Tracer.class)
public class NoopTracerAutoConfiguration {
@Bean
Tracer noopTracer() {
return Tracer.NOOP;
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Map;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.StringUtils;
/**
* {@link SpringBootCondition} to check whether tracing is enabled.
*
* @author Moritz Halbritter
* @see ConditionalOnEnabledTracing
*/
class OnEnabledTracingCondition extends SpringBootCondition {
private static final String GLOBAL_PROPERTY = "management.tracing.enabled";
private static final String EXPORTER_PROPERTY = "management.%s.tracing.export.enabled";
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String tracingExporter = getExporterName(metadata);
if (StringUtils.hasLength(tracingExporter)) {
Boolean exporterTracingEnabled = context.getEnvironment()
.getProperty(EXPORTER_PROPERTY.formatted(tracingExporter), Boolean.class);
if (exporterTracingEnabled != null) {
return new ConditionOutcome(exporterTracingEnabled,
ConditionMessage.forCondition(ConditionalOnEnabledTracing.class)
.because(EXPORTER_PROPERTY.formatted(tracingExporter) + " is " + exporterTracingEnabled));
}
}
Boolean globalTracingEnabled = context.getEnvironment().getProperty(GLOBAL_PROPERTY, Boolean.class);
if (globalTracingEnabled != null) {
return new ConditionOutcome(globalTracingEnabled,
ConditionMessage.forCondition(ConditionalOnEnabledTracing.class)
.because(GLOBAL_PROPERTY + " is " + globalTracingEnabled));
}
return ConditionOutcome.match(ConditionMessage.forCondition(ConditionalOnEnabledTracing.class)
.because("tracing is enabled by default"));
}
private static String getExporterName(AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnEnabledTracing.class.getName());
if (attributes == null) {
return null;
}
return (String) attributes.get("value");
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import io.micrometer.tracing.otel.bridge.EventPublishingContextWrapper;
import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.ContextStorage;
import io.opentelemetry.context.Scope;
import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@link ApplicationListener} to add an OpenTelemetry {@link ContextStorage} wrapper for
* {@link EventPublisher} bean support. A single {@link ContextStorage} wrapper is added
* on the {@link ApplicationStartingEvent} then updated with {@link EventPublisher} beans
* as needed.
* <p>
* The {@link #addWrapper()} method may also be called directly if the
* {@link ApplicationStartingEvent} isn't called early enough or isn't fired.
*
* @author Phillip Webb
* @since 4.0.0
* @see OpenTelemetryEventPublisherBeansTestExecutionListener
*/
public class OpenTelemetryEventPublisherBeansApplicationListener implements GenericApplicationListener {
private static final boolean OTEL_CONTEXT_PRESENT = ClassUtils.isPresent("io.opentelemetry.context.ContextStorage",
null);
private static final boolean MICROMETER_OTEL_PRESENT = ClassUtils
.isPresent("io.micrometer.tracing.otel.bridge.OtelTracer", null);
private static final AtomicBoolean added = new AtomicBoolean();
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public boolean supportsEventType(ResolvableType eventType) {
Class<?> type = eventType.getRawClass();
return (type != null) && (ApplicationStartingEvent.class.isAssignableFrom(type)
|| ContextRefreshedEvent.class.isAssignableFrom(type)
|| ContextClosedEvent.class.isAssignableFrom(type));
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (!isInstallable()) {
return;
}
if (event instanceof ApplicationStartingEvent) {
addWrapper();
}
if (event instanceof ContextRefreshedEvent contextRefreshedEvent) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
List<EventPublishingContextWrapper> publishers = applicationContext
.getBeansOfType(EventPublisher.class, true, false)
.values()
.stream()
.map(EventPublishingContextWrapper::new)
.toList();
Wrapper.instance.put(applicationContext, publishers);
}
if (event instanceof ContextClosedEvent contextClosedEvent) {
Wrapper.instance.remove(contextClosedEvent.getApplicationContext());
}
}
/**
* {@link ContextStorage#addWrapper(java.util.function.Function) Add} the
* {@link ContextStorage} wrapper to ensure that {@link EventPublisher
* EventPublishers} are propagated correctly.
*/
public static void addWrapper() {
if (isInstallable() && added.compareAndSet(false, true)) {
Wrapper.instance.addWrapper();
}
}
private static boolean isInstallable() {
return OTEL_CONTEXT_PRESENT && MICROMETER_OTEL_PRESENT;
}
/**
* Single instance class used to add the wrapper and manage the {@link EventPublisher}
* beans.
*/
static final class Wrapper {
static final Wrapper instance = new Wrapper();
private final MultiValueMap<ApplicationContext, EventPublishingContextWrapper> beans = new LinkedMultiValueMap<>();
private volatile ContextStorage storageDelegate;
private Wrapper() {
}
private void addWrapper() {
ContextStorage.addWrapper(Storage::new);
}
void put(ApplicationContext applicationContext, List<EventPublishingContextWrapper> publishers) {
synchronized (this) {
this.beans.addAll(applicationContext, publishers);
this.storageDelegate = null;
}
}
void remove(ApplicationContext applicationContext) {
synchronized (this) {
this.beans.remove(applicationContext);
this.storageDelegate = null;
}
}
ContextStorage getStorageDelegate(ContextStorage parent) {
ContextStorage delegate = this.storageDelegate;
if (delegate == null) {
synchronized (this) {
delegate = this.storageDelegate;
if (delegate == null) {
delegate = parent;
for (List<EventPublishingContextWrapper> publishers : this.beans.values()) {
for (EventPublishingContextWrapper publisher : publishers) {
delegate = publisher.apply(delegate);
}
}
this.storageDelegate = delegate;
}
}
}
return delegate;
}
/**
* {@link ContextStorage} that delegates to the {@link EventPublisher} beans.
*/
class Storage implements ContextStorage {
private final ContextStorage parent;
Storage(ContextStorage parent) {
this.parent = parent;
}
@Override
public Scope attach(Context toAttach) {
return getDelegate().attach(toAttach);
}
@Override
public Context current() {
return getDelegate().current();
}
@Override
public Context root() {
return getDelegate().root();
}
private ContextStorage getDelegate() {
return getStorageDelegate(this.parent);
}
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
/**
* JUnit {@link TestExecutionListener} to ensure
* {@link OpenTelemetryEventPublisherBeansApplicationListener#addWrapper()} is called as
* early as possible.
*
* @author Phillip Webb
* @since 4.0.0
* @see OpenTelemetryEventPublisherBeansApplicationListener
*/
public class OpenTelemetryEventPublisherBeansTestExecutionListener implements TestExecutionListener {
@Override
public void executionStarted(TestIdentifier testIdentifier) {
OpenTelemetryEventPublisherBeansApplicationListener.addWrapper();
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator;
import io.opentelemetry.context.propagation.TextMapPropagator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* OpenTelemetry propagation configurations. They are imported by
* {@link OpenTelemetryTracingAutoConfiguration}.
*
* @author Moritz Halbritter
*/
class OpenTelemetryPropagationConfigurations {
/**
* Propagates traces but no baggage.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty(name = "management.tracing.baggage.enabled", havingValue = false)
@EnableConfigurationProperties(TracingProperties.class)
static class PropagationWithoutBaggage {
@Bean
@ConditionalOnEnabledTracing
TextMapPropagator textMapPropagator(TracingProperties properties) {
return CompositeTextMapPropagator.create(properties.getPropagation(), null);
}
}
/**
* Propagates traces and baggage.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty(name = "management.tracing.baggage.enabled", matchIfMissing = true)
@EnableConfigurationProperties(TracingProperties.class)
static class PropagationWithBaggage {
private final TracingProperties tracingProperties;
PropagationWithBaggage(TracingProperties tracingProperties) {
this.tracingProperties = tracingProperties;
}
@Bean
@ConditionalOnEnabledTracing
TextMapPropagator textMapPropagatorWithBaggage(OtelCurrentTraceContext otelCurrentTraceContext) {
List<String> remoteFields = this.tracingProperties.getBaggage().getRemoteFields();
List<String> tagFields = this.tracingProperties.getBaggage().getTagFields();
BaggageTextMapPropagator baggagePropagator = new BaggageTextMapPropagator(remoteFields,
new OtelBaggageManager(otelCurrentTraceContext, remoteFields, tagFields));
return CompositeTextMapPropagator.create(this.tracingProperties.getPropagation(), baggagePropagator);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBooleanProperty(name = "management.tracing.baggage.correlation.enabled", matchIfMissing = true)
Slf4JBaggageEventListener otelSlf4JBaggageEventListener() {
return new Slf4JBaggageEventListener(this.tracingProperties.getBaggage().getCorrelation().getFields());
}
}
/**
* Propagates neither traces nor baggage.
*/
@Configuration(proxyBeanMethods = false)
static class NoPropagation {
@Bean
@ConditionalOnMissingBean
TextMapPropagator noopTextMapPropagator() {
return TextMapPropagator.noop();
}
}
}

View File

@@ -0,0 +1,205 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import io.micrometer.tracing.SpanCustomizer;
import io.micrometer.tracing.exporter.SpanExportingPredicate;
import io.micrometer.tracing.exporter.SpanFilter;
import io.micrometer.tracing.exporter.SpanReporter;
import io.micrometer.tracing.otel.bridge.CompositeSpanExporter;
import io.micrometer.tracing.otel.bridge.EventListener;
import io.micrometer.tracing.otel.bridge.OtelBaggageManager;
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
import io.micrometer.tracing.otel.bridge.OtelPropagator;
import io.micrometer.tracing.otel.bridge.OtelSpanCustomizer;
import io.micrometer.tracing.otel.bridge.OtelTracer;
import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher;
import io.micrometer.tracing.otel.bridge.Slf4JEventListener;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessorBuilder;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.util.CollectionUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry tracing.
*
* @author Moritz Halbritter
* @author Marcin Grzejszczak
* @author Yanming Zhou
* @since 4.0.0
*/
@AutoConfiguration(before = { MicrometerTracingAutoConfiguration.class, NoopTracerAutoConfiguration.class })
@ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class })
@EnableConfigurationProperties(TracingProperties.class)
@Import({ OpenTelemetryPropagationConfigurations.PropagationWithoutBaggage.class,
OpenTelemetryPropagationConfigurations.PropagationWithBaggage.class,
OpenTelemetryPropagationConfigurations.NoPropagation.class })
public class OpenTelemetryTracingAutoConfiguration {
private static final Log logger = LogFactory.getLog(OpenTelemetryTracingAutoConfiguration.class);
private final TracingProperties tracingProperties;
OpenTelemetryTracingAutoConfiguration(TracingProperties tracingProperties) {
this.tracingProperties = tracingProperties;
if (!CollectionUtils.isEmpty(this.tracingProperties.getBaggage().getLocalFields())) {
logger.warn("Local fields are not supported when using OpenTelemetry!");
}
}
@Bean
@ConditionalOnMissingBean
SdkTracerProvider otelSdkTracerProvider(Resource resource, SpanProcessors spanProcessors, Sampler sampler,
ObjectProvider<SdkTracerProviderBuilderCustomizer> customizers) {
SdkTracerProviderBuilder builder = SdkTracerProvider.builder().setSampler(sampler).setResource(resource);
spanProcessors.forEach(builder::addSpanProcessor);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
@Bean
@ConditionalOnMissingBean
ContextPropagators otelContextPropagators(ObjectProvider<TextMapPropagator> textMapPropagators) {
return ContextPropagators.create(TextMapPropagator.composite(textMapPropagators.orderedStream().toList()));
}
@Bean
@ConditionalOnMissingBean
Sampler otelSampler() {
Sampler rootSampler = Sampler.traceIdRatioBased(this.tracingProperties.getSampling().getProbability());
return Sampler.parentBased(rootSampler);
}
@Bean
@ConditionalOnMissingBean
SpanProcessors spanProcessors(ObjectProvider<SpanProcessor> spanProcessors) {
return SpanProcessors.of(spanProcessors.orderedStream().toList());
}
@Bean
@ConditionalOnMissingBean
BatchSpanProcessor otelSpanProcessor(SpanExporters spanExporters,
ObjectProvider<SpanExportingPredicate> spanExportingPredicates, ObjectProvider<SpanReporter> spanReporters,
ObjectProvider<SpanFilter> spanFilters, ObjectProvider<MeterProvider> meterProvider) {
TracingProperties.OpenTelemetry.Export properties = this.tracingProperties.getOpentelemetry().getExport();
CompositeSpanExporter spanExporter = new CompositeSpanExporter(spanExporters.list(),
spanExportingPredicates.orderedStream().toList(), spanReporters.orderedStream().toList(),
spanFilters.orderedStream().toList());
BatchSpanProcessorBuilder builder = BatchSpanProcessor.builder(spanExporter)
.setExportUnsampledSpans(properties.isIncludeUnsampled())
.setExporterTimeout(properties.getTimeout())
.setMaxExportBatchSize(properties.getMaxBatchSize())
.setMaxQueueSize(properties.getMaxQueueSize())
.setScheduleDelay(properties.getScheduleDelay());
meterProvider.ifAvailable(builder::setMeterProvider);
return builder.build();
}
@Bean
@ConditionalOnMissingBean
SpanExporters spanExporters(ObjectProvider<SpanExporter> spanExporters) {
return SpanExporters.of(spanExporters.orderedStream().toList());
}
@Bean
@ConditionalOnMissingBean
Tracer otelTracer(OpenTelemetry openTelemetry) {
return openTelemetry.getTracer("org.springframework.boot", SpringBootVersion.getVersion());
}
@Bean
@ConditionalOnMissingBean(io.micrometer.tracing.Tracer.class)
OtelTracer micrometerOtelTracer(Tracer tracer, EventPublisher eventPublisher,
OtelCurrentTraceContext otelCurrentTraceContext) {
List<String> remoteFields = this.tracingProperties.getBaggage().getRemoteFields();
List<String> tagFields = this.tracingProperties.getBaggage().getTagFields();
return new OtelTracer(tracer, otelCurrentTraceContext, eventPublisher,
new OtelBaggageManager(otelCurrentTraceContext, remoteFields, tagFields));
}
@Bean
@ConditionalOnMissingBean
OtelPropagator otelPropagator(ContextPropagators contextPropagators, Tracer tracer) {
return new OtelPropagator(contextPropagators, tracer);
}
@Bean
@ConditionalOnMissingBean
EventPublisher otelTracerEventPublisher(List<EventListener> eventListeners) {
return new OTelEventPublisher(eventListeners);
}
@Bean
@ConditionalOnMissingBean
OtelCurrentTraceContext otelCurrentTraceContext() {
return new OtelCurrentTraceContext();
}
@Bean
@ConditionalOnMissingBean
Slf4JEventListener otelSlf4JEventListener() {
return new Slf4JEventListener();
}
@Bean
@ConditionalOnMissingBean(SpanCustomizer.class)
OtelSpanCustomizer otelSpanCustomizer() {
return new OtelSpanCustomizer();
}
static class OTelEventPublisher implements EventPublisher {
private final List<EventListener> listeners;
OTelEventPublisher(List<EventListener> listeners) {
this.listeners = listeners;
}
@Override
public void publishEvent(Object event) {
for (EventListener listener : this.listeners) {
listener.onEvent(event);
}
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SdkTracerProviderBuilder;
/**
* Callback interface that can be used to customize the {@link SdkTracerProviderBuilder}
* that is used to create the auto-configured {@link SdkTracerProvider}.
*
* @author Yanming Zhou
* @since 4.0.0
*/
@FunctionalInterface
public interface SdkTracerProviderBuilderCustomizer {
/**
* Customize the given {@code builder}.
* @param builder the builder to customize
*/
void customize(SdkTracerProviderBuilder builder);
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import org.springframework.util.Assert;
/**
* A collection of {@link SpanExporter span exporters}.
*
* @author Moritz Halbritter
* @since 4.0.0
*/
@FunctionalInterface
public interface SpanExporters extends Iterable<SpanExporter> {
/**
* Returns the list of {@link SpanExporter span exporters}.
* @return the list of span exporters
*/
List<SpanExporter> list();
@Override
default Iterator<SpanExporter> iterator() {
return list().iterator();
}
@Override
default Spliterator<SpanExporter> spliterator() {
return list().spliterator();
}
/**
* Constructs a {@link SpanExporters} instance with the given {@link SpanExporter span
* exporters}.
* @param spanExporters the span exporters
* @return the constructed {@link SpanExporters} instance
*/
static SpanExporters of(SpanExporter... spanExporters) {
return of(Arrays.asList(spanExporters));
}
/**
* Constructs a {@link SpanExporters} instance with the given list of
* {@link SpanExporter span exporters}.
* @param spanExporters the list of span exporters
* @return the constructed {@link SpanExporters} instance
*/
static SpanExporters of(Collection<? extends SpanExporter> spanExporters) {
Assert.notNull(spanExporters, "'spanExporters' must not be null");
List<SpanExporter> copy = List.copyOf(spanExporters);
return () -> copy;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import io.opentelemetry.sdk.trace.SpanProcessor;
import org.springframework.util.Assert;
/**
* A collection of {@link SpanProcessor span processors}.
*
* @author Moritz Halbritter
* @since 4.0.0
*/
@FunctionalInterface
public interface SpanProcessors extends Iterable<SpanProcessor> {
/**
* Returns the list of {@link SpanProcessor span processors}.
* @return the list of span processors
*/
List<SpanProcessor> list();
@Override
default Iterator<SpanProcessor> iterator() {
return list().iterator();
}
@Override
default Spliterator<SpanProcessor> spliterator() {
return list().spliterator();
}
/**
* Constructs a {@link SpanProcessors} instance with the given {@link SpanProcessor
* span processors}.
* @param spanProcessors the span processors
* @return the constructed {@link SpanProcessors} instance
*/
static SpanProcessors of(SpanProcessor... spanProcessors) {
return of(Arrays.asList(spanProcessors));
}
/**
* Constructs a {@link SpanProcessors} instance with the given list of
* {@link SpanProcessor span processors}.
* @param spanProcessors the list of span processors
* @return the constructed {@link SpanProcessors} instance
*/
static SpanProcessors of(Collection<? extends SpanProcessor> spanProcessors) {
Assert.notNull(spanProcessors, "'spanProcessors' must not be null");
List<SpanProcessor> copy = List.copyOf(spanProcessors);
return () -> copy;
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.ArrayList;
import java.util.List;
import io.micrometer.core.instrument.observation.MeterObservationHandler;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler;
import io.micrometer.observation.ObservationRegistry.ObservationConfig;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler;
import io.micrometer.tracing.handler.TracingObservationHandler;
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
/**
* {@link ObservationHandlerGroup} that considers both {@link TracingObservationHandler}
* and {@link MeterObservationHandler} types as members. This group takes precedence over
* any regular {@link MeterObservationHandler} group in order to use ensure
* {@link TracingAwareMeterObservationHandler} wrapping is applied during registration.
*
* @author Phillip Webb
*/
class TracingAndMeterObservationHandlerGroup implements ObservationHandlerGroup {
private final Tracer tracer;
TracingAndMeterObservationHandlerGroup(Tracer tracer) {
this.tracer = tracer;
}
@Override
public boolean isMember(ObservationHandler<?> handler) {
return MeterObservationHandler.class.isInstance(handler) || TracingObservationHandler.class.isInstance(handler);
}
@Override
public int compareTo(ObservationHandlerGroup other) {
if (other instanceof TracingAndMeterObservationHandlerGroup) {
return 0;
}
return MeterObservationHandler.class.isAssignableFrom(other.handlerType()) ? -1 : 1;
}
@Override
public void registerMembers(ObservationConfig config, List<ObservationHandler<?>> members) {
List<ObservationHandler<?>> tracingHandlers = new ArrayList<>(members.size());
List<ObservationHandler<?>> metricsHandlers = new ArrayList<>(members.size());
for (ObservationHandler<?> member : members) {
if (member instanceof MeterObservationHandler<?> meterObservationHandler
&& !(member instanceof TracingAwareMeterObservationHandler<?>)) {
metricsHandlers.add(new TracingAwareMeterObservationHandler<>(meterObservationHandler, this.tracer));
}
else {
tracingHandlers.add(member);
}
}
registerHandlers(config, tracingHandlers);
registerHandlers(config, metricsHandlers);
}
private void registerHandlers(ObservationConfig config, List<ObservationHandler<?>> handlers) {
if (handlers.size() == 1) {
config.observationHandler(handlers.get(0));
}
else if (!handlers.isEmpty()) {
config.observationHandler(new FirstMatchingCompositeObservationHandler(handlers));
}
}
@Override
public Class<?> handlerType() {
return TracingObservationHandler.class;
}
}

View File

@@ -0,0 +1,390 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for tracing.
*
* @author Moritz Halbritter
* @author Jonatan Ivanov
* @since 4.0.0
*/
@ConfigurationProperties("management.tracing")
public class TracingProperties {
/**
* Sampling configuration.
*/
private final Sampling sampling = new Sampling();
/**
* Baggage configuration.
*/
private final Baggage baggage = new Baggage();
/**
* Propagation configuration.
*/
private final Propagation propagation = new Propagation();
/**
* Brave configuration.
*/
private final Brave brave = new Brave();
/**
* OpenTelemetry configuration.
*/
private final OpenTelemetry opentelemetry = new OpenTelemetry();
public Sampling getSampling() {
return this.sampling;
}
public Baggage getBaggage() {
return this.baggage;
}
public Propagation getPropagation() {
return this.propagation;
}
public Brave getBrave() {
return this.brave;
}
public OpenTelemetry getOpentelemetry() {
return this.opentelemetry;
}
public static class Sampling {
/**
* Probability in the range from 0.0 to 1.0 that a trace will be sampled.
*/
private float probability = 0.10f;
public float getProbability() {
return this.probability;
}
public void setProbability(float probability) {
this.probability = probability;
}
}
public static class Baggage {
/**
* Whether to enable Micrometer Tracing baggage propagation.
*/
private boolean enabled = true;
/**
* Correlation configuration.
*/
private Correlation correlation = new Correlation();
/**
* List of fields that are referenced the same in-process as it is on the wire.
* For example, the field "x-vcap-request-id" would be set as-is including the
* prefix.
*/
private List<String> remoteFields = new ArrayList<>();
/**
* List of fields that should be accessible within the JVM process but not
* propagated over the wire. Local fields are not supported with OpenTelemetry.
*/
private List<String> localFields = new ArrayList<>();
/**
* List of fields that should automatically become tags.
*/
private List<String> tagFields = new ArrayList<>();
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Correlation getCorrelation() {
return this.correlation;
}
public void setCorrelation(Correlation correlation) {
this.correlation = correlation;
}
public List<String> getRemoteFields() {
return this.remoteFields;
}
public List<String> getLocalFields() {
return this.localFields;
}
public List<String> getTagFields() {
return this.tagFields;
}
public void setRemoteFields(List<String> remoteFields) {
this.remoteFields = remoteFields;
}
public void setLocalFields(List<String> localFields) {
this.localFields = localFields;
}
public void setTagFields(List<String> tagFields) {
this.tagFields = tagFields;
}
public static class Correlation {
/**
* Whether to enable correlation of the baggage context with logging contexts.
*/
private boolean enabled = true;
/**
* List of fields that should be correlated with the logging context. That
* means that these fields would end up as key-value pairs in e.g. MDC.
*/
private List<String> fields = new ArrayList<>();
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getFields() {
return this.fields;
}
public void setFields(List<String> fields) {
this.fields = fields;
}
}
}
public static class Propagation {
/**
* Tracing context propagation types produced and consumed by the application.
* Setting this property overrides the more fine-grained propagation type
* properties.
*/
private List<PropagationType> type;
/**
* Tracing context propagation types produced by the application.
*/
private List<PropagationType> produce = List.of(PropagationType.W3C);
/**
* Tracing context propagation types consumed by the application.
*/
private List<PropagationType> consume = List.of(PropagationType.values());
public void setType(List<PropagationType> type) {
this.type = type;
}
public void setProduce(List<PropagationType> produce) {
this.produce = produce;
}
public void setConsume(List<PropagationType> consume) {
this.consume = consume;
}
public List<PropagationType> getType() {
return this.type;
}
public List<PropagationType> getProduce() {
return this.produce;
}
public List<PropagationType> getConsume() {
return this.consume;
}
/**
* Returns the effective context propagation types produced by the application.
* This will be {@link #getType()} if set or {@link #getProduce()} otherwise.
* @return the effective context propagation types produced by the application
*/
List<PropagationType> getEffectiveProducedTypes() {
return (this.type != null) ? this.type : this.produce;
}
/**
* Returns the effective context propagation types consumed by the application.
* This will be {@link #getType()} if set or {@link #getConsume()} otherwise.
* @return the effective context propagation types consumed by the application
*/
List<PropagationType> getEffectiveConsumedTypes() {
return (this.type != null) ? this.type : this.consume;
}
/**
* Supported propagation types. The declared order of the values matter.
*/
public enum PropagationType {
/**
* <a href="https://www.w3.org/TR/trace-context/">W3C</a> propagation.
*/
W3C,
/**
* <a href="https://github.com/openzipkin/b3-propagation#single-header">B3
* single header</a> propagation.
*/
B3,
/**
* <a href="https://github.com/openzipkin/b3-propagation#multiple-headers">B3
* multiple headers</a> propagation.
*/
B3_MULTI
}
}
public static class Brave {
/**
* Whether the propagation type and tracing backend support sharing the span ID
* between client and server spans. Requires B3 propagation and a compatible
* backend.
*/
private boolean spanJoiningSupported = false;
public boolean isSpanJoiningSupported() {
return this.spanJoiningSupported;
}
public void setSpanJoiningSupported(boolean spanJoiningSupported) {
this.spanJoiningSupported = spanJoiningSupported;
}
}
public static class OpenTelemetry {
/**
* Span export configuration.
*/
private final Export export = new Export();
public Export getExport() {
return this.export;
}
public static class Export {
/**
* Whether unsampled spans should be exported.
*/
private boolean includeUnsampled;
/**
* Maximum time an export will be allowed to run before being cancelled.
*/
private Duration timeout = Duration.ofSeconds(30);
/**
* Maximum batch size for each export. This must be less than or equal to
* 'maxQueueSize'.
*/
private int maxBatchSize = 512;
/**
* Maximum number of spans that are kept in the queue before they will be
* dropped.
*/
private int maxQueueSize = 2048;
/**
* The delay interval between two consecutive exports.
*/
private Duration scheduleDelay = Duration.ofSeconds(5);
public boolean isIncludeUnsampled() {
return this.includeUnsampled;
}
public void setIncludeUnsampled(boolean includeUnsampled) {
this.includeUnsampled = includeUnsampled;
}
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public int getMaxBatchSize() {
return this.maxBatchSize;
}
public void setMaxBatchSize(int maxBatchSize) {
this.maxBatchSize = maxBatchSize;
}
public int getMaxQueueSize() {
return this.maxQueueSize;
}
public void setMaxQueueSize(int maxQueueSize) {
this.maxQueueSize = maxQueueSize;
}
public Duration getScheduleDelay() {
return this.scheduleDelay;
}
public void setScheduleDelay(Duration scheduleDelay) {
this.scheduleDelay = scheduleDelay;
}
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link OtlpGrpcSpanExporterBuilder} whilst retaining default auto-configuration.
*
* @author Dmytro Nosan
* @since 4.0.0
*/
@FunctionalInterface
public interface OtlpGrpcSpanExporterBuilderCustomizer {
/**
* Customize the {@link OtlpGrpcSpanExporterBuilder}.
* @param builder the builder to customize
*/
void customize(OtlpGrpcSpanExporterBuilder builder);
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link OtlpHttpSpanExporterBuilder} whilst retaining default auto-configuration.
*
* @author Dmytro Nosan
* @since 4.0.0
*/
@FunctionalInterface
public interface OtlpHttpSpanExporterBuilderCustomizer {
/**
* Customize the {@link OtlpHttpSpanExporterBuilder}.
* @param builder the builder to customize
*/
void customize(OtlpHttpSpanExporterBuilder builder);
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import io.micrometer.tracing.otel.bridge.OtelTracer;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for exporting traces with OTLP.
* Brave does not support OTLP, so we only configure it for OpenTelemetry. OTLP defines
* three transports that are supported: gRPC (/protobuf), HTTP/protobuf, HTTP/JSON. From
* these transports HTTP/JSON is not supported by the OTel Java SDK, and it seems there
* are no plans supporting it in the future, see: <a href=
* "https://github.com/open-telemetry/opentelemetry-java/issues/3651">opentelemetry-java#3651</a>.
* Because this class configures components from the OTel SDK, it can't support HTTP/JSON.
* By default, we auto-configure HTTP/protobuf. If you want to use gRPC, you need to set
* {@code management.otlp.tracing.transport=grpc}. If you define a
* {@link OtlpHttpSpanExporter} or {@link OtlpGrpcSpanExporter}, this auto-configuration
* will back off.
*
* @author Jonatan Ivanov
* @author Moritz Halbritter
* @author Eddú Meléndez
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass({ OtelTracer.class, SdkTracerProvider.class, OpenTelemetry.class, OtlpHttpSpanExporter.class })
@EnableConfigurationProperties(OtlpTracingProperties.class)
@Import({ OtlpTracingConfigurations.ConnectionDetails.class, OtlpTracingConfigurations.Exporters.class })
public class OtlpTracingAutoConfiguration {
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import java.util.Locale;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.tracing.autoconfigure.ConditionalOnEnabledTracing;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
/**
* Configurations imported by {@link OtlpTracingAutoConfiguration}.
*
* @author Moritz Halbritter
* @author Eddú Meléndez
*/
final class OtlpTracingConfigurations {
@Configuration(proxyBeanMethods = false)
static class ConnectionDetails {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("management.otlp.tracing.endpoint")
OtlpTracingConnectionDetails otlpTracingConnectionDetails(OtlpTracingProperties properties) {
return new PropertiesOtlpTracingConnectionDetails(properties);
}
/**
* Adapts {@link OtlpTracingProperties} to {@link OtlpTracingConnectionDetails}.
*/
static class PropertiesOtlpTracingConnectionDetails implements OtlpTracingConnectionDetails {
private final OtlpTracingProperties properties;
PropertiesOtlpTracingConnectionDetails(OtlpTracingProperties properties) {
this.properties = properties;
}
@Override
public String getUrl(Transport transport) {
Assert.state(transport == this.properties.getTransport(),
"Requested transport %s doesn't match configured transport %s".formatted(transport,
this.properties.getTransport()));
return this.properties.getEndpoint();
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({ OtlpGrpcSpanExporter.class, OtlpHttpSpanExporter.class })
@ConditionalOnBean(OtlpTracingConnectionDetails.class)
@ConditionalOnEnabledTracing("otlp")
static class Exporters {
@Bean
@ConditionalOnProperty(name = "management.otlp.tracing.transport", havingValue = "http", matchIfMissing = true)
OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties,
OtlpTracingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpHttpSpanExporterBuilderCustomizer> customizers) {
OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder()
.setEndpoint(connectionDetails.getUrl(Transport.HTTP))
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT));
properties.getHeaders().forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
@Bean
@ConditionalOnProperty(name = "management.otlp.tracing.transport", havingValue = "grpc")
OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties,
OtlpTracingConnectionDetails connectionDetails, ObjectProvider<MeterProvider> meterProvider,
ObjectProvider<OtlpGrpcSpanExporterBuilderCustomizer> customizers) {
OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder()
.setEndpoint(connectionDetails.getUrl(Transport.GRPC))
.setTimeout(properties.getTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT));
properties.getHeaders().forEach(builder::addHeader);
meterProvider.ifAvailable(builder::setMeterProvider);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
/**
* Details required to establish a connection to an OpenTelemetry service.
*
* @author Eddú Meléndez
* @author Moritz Halbritter
* @since 4.0.0
*/
public interface OtlpTracingConnectionDetails extends ConnectionDetails {
/**
* Address to where tracing will be published.
* @return the address to where tracing will be published
* @deprecated since 3.4.0 for removal in 4.0.0 in favor of {@link #getUrl(Transport)}
*/
@Deprecated(since = "3.4.0", forRemoval = true)
default String getUrl() {
return getUrl(Transport.HTTP);
}
/**
* Address to where tracing will be published.
* @param transport the transport to use
* @return the address to where tracing will be published
* @since 3.4.0
*/
String getUrl(Transport transport);
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for exporting traces using OTLP.
*
* @author Jonatan Ivanov
* @since 4.0.0
*/
@ConfigurationProperties("management.otlp.tracing")
public class OtlpTracingProperties {
/**
* URL to the OTel collector's HTTP API.
*/
private String endpoint;
/**
* Call timeout for the OTel Collector to process an exported batch of data. This
* timeout spans the entire call: resolving DNS, connecting, writing the request body,
* server processing, and reading the response body. If the call requires redirects or
* retries all must complete within one timeout period.
*/
private Duration timeout = Duration.ofSeconds(10);
/**
* Connect timeout for the OTel collector connection.
*/
private Duration connectTimeout = Duration.ofSeconds(10);
/**
* Transport used to send the spans.
*/
private Transport transport = Transport.HTTP;
/**
* Method used to compress the payload.
*/
private Compression compression = Compression.NONE;
/**
* Custom HTTP headers you want to pass to the collector, for example auth headers.
*/
private Map<String, String> headers = new HashMap<>();
public String getEndpoint() {
return this.endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public Duration getTimeout() {
return this.timeout;
}
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
public Duration getConnectTimeout() {
return this.connectTimeout;
}
public void setConnectTimeout(Duration connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Transport getTransport() {
return this.transport;
}
public void setTransport(Transport transport) {
this.transport = transport;
}
public Compression getCompression() {
return this.compression;
}
public void setCompression(Compression compression) {
this.compression = compression;
}
public Map<String, String> getHeaders() {
return this.headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public enum Compression {
/**
* Gzip compression.
*/
GZIP,
/**
* No compression.
*/
NONE
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
/**
* Transport used to send OTLP data.
*
* @author Moritz Halbritter
* @since 4.0.0
*/
public enum Transport {
/**
* HTTP transport.
*/
HTTP,
/**
* gRPC transport.
*/
GRPC
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.
*/
/**
* Auto-configuration for exporting traces with OTLP.
*/
package org.springframework.boot.tracing.autoconfigure.otlp;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.
*/
/**
* Auto-configuration for Micrometer Tracing.
*/
package org.springframework.boot.tracing.autoconfigure;

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.prometheus;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import io.prometheus.metrics.tracer.common.SpanContext;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.tracing.autoconfigure.MicrometerTracingAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.util.function.SingletonSupplier;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Prometheus Exemplars with
* Micrometer Tracing.
*
* @author Jonatan Ivanov
* @since 4.0.0
*/
@AutoConfiguration(
beforeName = "org.springframework.boot.metrics.autoconfigure.export.prometheus.PrometheusMetricsExportAutoConfiguration",
after = MicrometerTracingAutoConfiguration.class)
@ConditionalOnBean(Tracer.class)
@ConditionalOnClass({ Tracer.class, SpanContext.class })
public class PrometheusExemplarsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SpanContext spanContext(ObjectProvider<Tracer> tracerProvider) {
return new LazyTracingSpanContext(tracerProvider);
}
/**
* Since the MeterRegistry can depend on the {@link Tracer} (Exemplars) and the
* {@link Tracer} can depend on the MeterRegistry (recording metrics), this
* {@link SpanContext} breaks the cycle by lazily loading the {@link Tracer}.
*/
static class LazyTracingSpanContext implements SpanContext {
private final SingletonSupplier<Tracer> tracer;
LazyTracingSpanContext(ObjectProvider<Tracer> tracerProvider) {
this.tracer = SingletonSupplier.of(tracerProvider::getObject);
}
@Override
public String getCurrentTraceId() {
Span currentSpan = currentSpan();
return (currentSpan != null) ? currentSpan.context().traceId() : null;
}
@Override
public String getCurrentSpanId() {
Span currentSpan = currentSpan();
return (currentSpan != null) ? currentSpan.context().spanId() : null;
}
@Override
public boolean isCurrentSpanSampled() {
Span currentSpan = currentSpan();
if (currentSpan == null) {
return false;
}
Boolean sampled = currentSpan.context().sampled();
return sampled != null && sampled;
}
@Override
public void markCurrentSpanAsExemplar() {
}
private Span currentSpan() {
return this.tracer.obtain().currentSpan();
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.
*/
/**
* Auto-configuration for Prometheus Exemplars with Micrometer Tracing.
*/
package org.springframework.boot.tracing.autoconfigure.prometheus;

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.zipkin;
import brave.Tag;
import brave.Tags;
import brave.handler.MutableSpan;
import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter;
import zipkin2.Span;
import zipkin2.reporter.BytesEncoder;
import zipkin2.reporter.BytesMessageSender;
import zipkin2.reporter.Encoding;
import zipkin2.reporter.SpanBytesEncoder;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;
import zipkin2.reporter.brave.MutableSpanBytesEncoder;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.tracing.autoconfigure.ConditionalOnEnabledTracing;
import org.springframework.boot.tracing.autoconfigure.zipkin.ZipkinTracingAutoConfiguration.BraveConfiguration;
import org.springframework.boot.tracing.autoconfigure.zipkin.ZipkinTracingAutoConfiguration.OpenTelemetryConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Zipkin tracing.
*
* @author Moritz Halbritter
* @author Stefan Bratanov
* @author Wick Dynex
* @author Phillip Webb
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass(Encoding.class)
@Import({ BraveConfiguration.class, OpenTelemetryConfiguration.class })
public class ZipkinTracingAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AsyncZipkinSpanHandler.class)
static class BraveConfiguration {
@Bean
@ConditionalOnMissingBean(value = MutableSpan.class, parameterizedContainer = BytesEncoder.class)
BytesEncoder<MutableSpan> mutableSpanBytesEncoder(Encoding encoding,
ObjectProvider<Tag<Throwable>> throwableTagProvider) {
Tag<Throwable> throwableTag = throwableTagProvider.getIfAvailable(() -> Tags.ERROR);
return MutableSpanBytesEncoder.create(encoding, throwableTag);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(BytesMessageSender.class)
@ConditionalOnEnabledTracing("zipkin")
AsyncZipkinSpanHandler asyncZipkinSpanHandler(BytesMessageSender sender,
BytesEncoder<MutableSpan> mutableSpanBytesEncoder) {
return AsyncZipkinSpanHandler.newBuilder(sender).build(mutableSpanBytesEncoder);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ ZipkinSpanExporter.class, Span.class })
static class OpenTelemetryConfiguration {
@Bean
@ConditionalOnMissingBean(value = Span.class, parameterizedContainer = BytesEncoder.class)
BytesEncoder<Span> spanBytesEncoder(Encoding encoding) {
return SpanBytesEncoder.forEncoding(encoding);
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(BytesMessageSender.class)
@ConditionalOnEnabledTracing("zipkin")
ZipkinSpanExporter zipkinSpanExporter(BytesMessageSender sender, BytesEncoder<Span> spanBytesEncoder) {
return ZipkinSpanExporter.builder().setSender(sender).setEncoder(spanBytesEncoder).build();
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.
*/
/**
* Auto-configuration for tracing with Zipkin.
*/
package org.springframework.boot.tracing.autoconfigure.zipkin;

View File

@@ -0,0 +1,35 @@
{
"groups": [],
"properties": [
{
"name": "management.otlp.tracing.export.enabled",
"type": "java.lang.Boolean",
"description": "Whether auto-configuration of tracing is enabled to export OTLP traces."
},
{
"name": "management.tracing.enabled",
"type": "java.lang.Boolean",
"description": "Whether auto-configuration of tracing is enabled to export and propagate traces.",
"defaultValue": true
},
{
"name": "management.tracing.propagation.consume",
"defaultValue": [
"W3C",
"B3",
"B3_MULTI"
]
},
{
"name": "management.tracing.propagation.produce",
"defaultValue": [
"W3C"
]
},
{
"name": "management.zipkin.tracing.export.enabled",
"type": "java.lang.Boolean",
"description": "Whether auto-configuration of tracing is enabled to export Zipkin traces."
}
]
}

View File

@@ -0,0 +1 @@
org.springframework.boot.tracing.autoconfigure.OpenTelemetryEventPublisherBeansTestExecutionListener

View File

@@ -0,0 +1,7 @@
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.tracing.autoconfigure.LogCorrelationEnvironmentPostProcessor
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.tracing.autoconfigure.OpenTelemetryEventPublisherBeansApplicationListener

View File

@@ -0,0 +1,7 @@
org.springframework.boot.tracing.autoconfigure.BraveAutoConfiguration
org.springframework.boot.tracing.autoconfigure.MicrometerTracingAutoConfiguration
org.springframework.boot.tracing.autoconfigure.NoopTracerAutoConfiguration
org.springframework.boot.tracing.autoconfigure.OpenTelemetryTracingAutoConfiguration
org.springframework.boot.tracing.autoconfigure.otlp.OtlpTracingAutoConfiguration
org.springframework.boot.tracing.autoconfigure.prometheus.PrometheusExemplarsAutoConfiguration
org.springframework.boot.tracing.autoconfigure.zipkin.ZipkinTracingAutoConfiguration

View File

@@ -0,0 +1,297 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.function.Supplier;
import io.micrometer.tracing.BaggageInScope;
import io.micrometer.tracing.BaggageManager;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.Tracer;
import io.opentelemetry.context.Context;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.slf4j.MDC;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for Baggage propagation with Brave and OpenTelemetry using W3C and B3 propagation
* formats.
*
* @author Marcin Grzejszczak
* @author Moritz Halbritter
*/
@ForkedClassPath
class BaggagePropagationIntegrationTests {
private static final String COUNTRY_CODE = "country-code";
private static final String BUSINESS_PROCESS = "bp";
@BeforeEach
@AfterEach
void setup() {
MDC.clear();
}
@ParameterizedTest
@EnumSource
void shouldSetEntriesToMdcFromSpanWithBaggage(AutoConfig autoConfig) {
autoConfig.get().run((context) -> {
Tracer tracer = tracer(context);
Span span = createSpan(tracer);
BaggageManager baggageManager = baggageManager(context);
assertThatTracingContextIsInitialized(autoConfig);
try (Tracer.SpanInScope scope = tracer.withSpan(span.start())) {
assertMdcValue("traceId", span.context().traceId());
try (BaggageInScope fo = baggageManager.createBaggageInScope(span.context(), COUNTRY_CODE, "FO");
BaggageInScope alm = baggageManager.createBaggageInScope(span.context(), BUSINESS_PROCESS,
"ALM")) {
assertMdcValue(COUNTRY_CODE, "FO");
assertMdcValue(BUSINESS_PROCESS, "ALM");
}
}
finally {
span.end();
}
assertThatMdcContainsUnsetTraceId(autoConfig);
assertUnsetMdc(COUNTRY_CODE);
assertUnsetMdc(BUSINESS_PROCESS);
});
}
@ParameterizedTest
@EnumSource
void shouldRemoveEntriesFromMdcForNullSpan(AutoConfig autoConfig) {
autoConfig.get().run((context) -> {
Tracer tracer = tracer(context);
Span span = createSpan(tracer);
BaggageManager baggageManager = baggageManager(context);
assertThatTracingContextIsInitialized(autoConfig);
try (Tracer.SpanInScope scope = tracer.withSpan(span.start())) {
assertMdcValue("traceId", span.context().traceId());
try (BaggageInScope fo = baggageManager.createBaggageInScope(span.context(), COUNTRY_CODE, "FO")) {
assertMdcValue(COUNTRY_CODE, "FO");
try (Tracer.SpanInScope scope2 = tracer.withSpan(null)) {
assertThatMdcContainsUnsetTraceId(autoConfig);
assertUnsetMdc(COUNTRY_CODE);
}
assertMdcValue("traceId", span.context().traceId());
assertMdcValue(COUNTRY_CODE, "FO");
}
}
finally {
span.end();
}
assertThatMdcContainsUnsetTraceId(autoConfig);
assertUnsetMdc(COUNTRY_CODE);
});
}
private Span createSpan(Tracer tracer) {
return tracer.nextSpan().name("span");
}
private Tracer tracer(ApplicationContext context) {
return context.getBean(Tracer.class);
}
private BaggageManager baggageManager(ApplicationContext context) {
return context.getBean(BaggageManager.class);
}
private void assertThatTracingContextIsInitialized(AutoConfig autoConfig) {
if (autoConfig.isOtel()) {
assertThat(Context.current()).isEqualTo(Context.root());
}
}
private void assertThatMdcContainsUnsetTraceId(AutoConfig autoConfig) {
boolean eitherOtelOrBrave = autoConfig.isOtel() || autoConfig.isBrave();
assertThat(eitherOtelOrBrave).isTrue();
if (autoConfig.isOtel()) {
ThrowingConsumer<String> isNull = (traceId) -> assertThat(traceId).isNull();
ThrowingConsumer<String> isZero = (traceId) -> assertThat(traceId)
.isEqualTo("00000000000000000000000000000000");
assertThat(MDC.get("traceId")).satisfiesAnyOf(isNull, isZero);
}
if (autoConfig.isBrave()) {
assertThat(MDC.get("traceId")).isNull();
}
}
private void assertUnsetMdc(String key) {
assertThat(MDC.get(key)).as("MDC[%s]", key).isNull();
}
private void assertMdcValue(String key, String expected) {
assertThat(MDC.get(key)).as("MDC[%s]", key).isEqualTo(expected);
}
enum AutoConfig implements Supplier<ApplicationContextRunner> {
BRAVE_DEFAULT {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
.withPropertyValues("management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
OTEL_DEFAULT {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.tracing.autoconfigure.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
BRAVE_W3C {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=W3C",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
OTEL_W3C {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.tracing.autoconfigure.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=W3C",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
BRAVE_B3 {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=B3",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
BRAVE_B3_MULTI {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=B3_MULTI",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
OTEL_B3 {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.tracing.autoconfigure.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=B3",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
OTEL_B3_MULTI {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.tracing.autoconfigure.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=B3_MULTI",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
},
BRAVE_LOCAL_FIELDS {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class))
.withPropertyValues("management.tracing.baggage.local-fields=country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
}
};
boolean isOtel() {
return name().startsWith("OTEL_");
}
boolean isBrave() {
return name().startsWith("BRAVE_");
}
}
static class OtelApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener());
}
}
}

View File

@@ -0,0 +1,537 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import brave.Span;
import brave.SpanCustomizer;
import brave.Tracer;
import brave.Tracing;
import brave.baggage.BaggagePropagation;
import brave.baggage.CorrelationScopeConfig.SingleCorrelationField;
import brave.handler.SpanHandler;
import brave.propagation.CurrentTraceContext;
import brave.propagation.CurrentTraceContext.ScopeDecorator;
import brave.propagation.Propagation;
import brave.propagation.Propagation.Factory;
import brave.propagation.TraceContext;
import brave.sampler.Sampler;
import io.micrometer.observation.Observation;
import io.micrometer.observation.Observation.Scope;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.brave.bridge.BraveBaggageManager;
import io.micrometer.tracing.brave.bridge.BraveSpanCustomizer;
import io.micrometer.tracing.brave.bridge.BraveTracer;
import io.micrometer.tracing.brave.bridge.CompositeSpanHandler;
import io.micrometer.tracing.brave.bridge.W3CPropagation;
import io.micrometer.tracing.exporter.SpanExportingPredicate;
import io.micrometer.tracing.exporter.SpanFilter;
import io.micrometer.tracing.exporter.SpanReporter;
import org.assertj.core.api.Assertions;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.properties.IncompatibleConfigurationException;
import org.springframework.boot.observation.autoconfigure.ObservationAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.tracing.autoconfigure.BraveAutoConfigurationTests.SpanHandlerConfiguration.AdditionalSpanHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BraveAutoConfiguration}.
*
* @author Moritz Halbritter
* @author Jonatan Ivanov
*/
class BraveAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(BraveAutoConfiguration.class));
@Test
void shouldSupplyDefaultBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(BraveAutoConfiguration.class);
assertThat(context).hasSingleBean(Tracing.class);
assertThat(context).hasSingleBean(Tracer.class);
assertThat(context).hasSingleBean(CurrentTraceContext.class);
assertThat(context).hasSingleBean(Factory.class);
assertThat(context).hasSingleBean(Sampler.class);
assertThat(context).hasSingleBean(BraveTracer.class);
assertThat(context).hasSingleBean(Propagation.Factory.class);
assertThat(context).hasSingleBean(BaggagePropagation.FactoryBuilder.class);
assertThat(context).hasSingleBean(BraveTracer.class);
assertThat(context).hasSingleBean(CompositeSpanHandler.class);
assertThat(context).hasSingleBean(SpanCustomizer.class);
assertThat(context).hasSingleBean(BraveSpanCustomizer.class);
});
}
@Test
void shouldBackOffOnCustomBeans() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> {
assertThat(context).hasBean("customTracing");
assertThat(context).hasSingleBean(Tracing.class);
assertThat(context).hasBean("customTracer");
assertThat(context).hasSingleBean(Tracer.class);
assertThat(context).hasBean("customCurrentTraceContext");
assertThat(context).hasSingleBean(CurrentTraceContext.class);
assertThat(context).hasBean("customFactory");
assertThat(context).hasSingleBean(Factory.class);
assertThat(context).hasBean("customSampler");
assertThat(context).hasSingleBean(Sampler.class);
assertThat(context).hasBean("customMicrometerTracer");
assertThat(context).hasSingleBean(io.micrometer.tracing.Tracer.class);
assertThat(context).hasBean("customBraveBaggageManager");
assertThat(context).hasSingleBean(BraveBaggageManager.class);
assertThat(context).hasBean("customCompositeSpanHandler");
assertThat(context).hasSingleBean(CompositeSpanHandler.class);
assertThat(context).hasBean("customSpanCustomizer");
assertThat(context).hasSingleBean(SpanCustomizer.class);
assertThat(context).hasBean("customMicrometerSpanCustomizer");
assertThat(context).hasSingleBean(io.micrometer.tracing.SpanCustomizer.class);
});
}
@Test
void shouldSupplyMicrometerBeans() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(BraveTracer.class));
}
@Test
void shouldNotSupplyBeansIfBraveIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("brave"))
.run((context) -> assertThat(context).doesNotHaveBean(BraveAutoConfiguration.class));
}
@Test
void shouldNotSupplyBeansIfMicrometerIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer"))
.run((context) -> assertThat(context).doesNotHaveBean(BraveAutoConfiguration.class));
}
@Test
void shouldSupplyW3CPropagationFactoryByDefault() {
this.contextRunner.run((context) -> {
assertThat(context).hasBean("propagationFactory");
Factory factory = context.getBean(Factory.class);
Stream<Class<?>> injectors = getInjectors(factory).stream().map(Object::getClass);
assertThat(injectors).containsExactly(W3CPropagation.class);
assertThat(context).hasSingleBean(BaggagePropagation.FactoryBuilder.class);
});
}
@Test
void shouldSupplyB3PropagationFactoryViaProperty() {
this.contextRunner.withPropertyValues("management.tracing.propagation.type=B3").run((context) -> {
assertThat(context).hasBean("propagationFactory");
Factory factory = context.getBean(Factory.class);
List<Factory> injectors = getInjectors(factory);
assertThat(injectors).extracting(Factory::toString).containsExactly("B3Propagation");
assertThat(context).hasSingleBean(BaggagePropagation.FactoryBuilder.class);
});
}
@Test
void shouldUseB3SingleWithParentWhenPropagationTypeIsB3() {
this.contextRunner
.withPropertyValues("management.tracing.propagation.type=B3", "management.tracing.sampling.probability=1.0")
.run((context) -> {
Propagation<String> propagation = context.getBean(Factory.class).get();
Tracer tracer = context.getBean(Tracing.class).tracer();
Span child;
Span parent = tracer.nextSpan().name("parent");
try (Tracer.SpanInScope ignored = tracer.withSpanInScope(parent.start())) {
child = tracer.nextSpan().name("child");
child.start().finish();
}
finally {
parent.finish();
}
Map<String, String> map = new HashMap<>();
TraceContext childContext = child.context();
propagation.injector(this::injectToMap).inject(childContext, map);
assertThat(map).containsExactly(Map.entry("b3", "%s-%s-1-%s".formatted(childContext.traceIdString(),
childContext.spanIdString(), childContext.parentIdString())));
});
}
@Test
void shouldNotSupplyCorrelationScopeDecoratorIfBaggageDisabled() {
this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean("correlationScopeDecorator"));
}
@Test
void shouldSupplyW3CWithoutBaggageByDefaultIfBaggageDisabled() {
this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false").run((context) -> {
assertThat(context).hasBean("propagationFactory");
Factory factory = context.getBean(Factory.class);
Stream<Class<?>> injectors = getInjectors(factory).stream().map(Object::getClass);
assertThat(injectors).containsExactly(W3CPropagation.class);
assertThat(context).doesNotHaveBean(BaggagePropagation.FactoryBuilder.class);
});
}
@Test
void shouldSupplyB3WithoutBaggageIfBaggageDisabledAndB3Picked() {
this.contextRunner
.withPropertyValues("management.tracing.baggage.enabled=false", "management.tracing.propagation.type=B3")
.run((context) -> {
assertThat(context).hasBean("propagationFactory");
Factory factory = context.getBean(Factory.class);
List<Factory> injectors = getInjectors(factory);
assertThat(injectors).extracting(Factory::toString).containsExactly("B3Propagation");
assertThat(context).doesNotHaveBean(BaggagePropagation.FactoryBuilder.class);
});
}
@Test
void shouldNotApplyCorrelationFieldsIfBaggageCorrelationDisabled() {
this.contextRunner
.withPropertyValues("management.tracing.baggage.correlation.enabled=false",
"management.tracing.baggage.correlation.fields=alpha,bravo")
.run((context) -> {
ScopeDecorator scopeDecorator = context.getBean(ScopeDecorator.class);
assertThat(scopeDecorator)
.extracting("fields", InstanceOfAssertFactories.array(SingleCorrelationField[].class))
.hasSize(2);
});
}
@Test
void shouldApplyCorrelationFieldsIfBaggageCorrelationEnabled() {
this.contextRunner
.withPropertyValues("management.tracing.baggage.correlation.enabled=true",
"management.tracing.baggage.correlation.fields=alpha,bravo")
.run((context) -> {
ScopeDecorator scopeDecorator = context.getBean(ScopeDecorator.class);
assertThat(scopeDecorator)
.extracting("fields", InstanceOfAssertFactories.array(SingleCorrelationField[].class))
.hasSize(4);
});
}
@Test
void shouldSupplyMdcCorrelationScopeDecoratorIfBaggageCorrelationDisabled() {
this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=false")
.run((context) -> assertThat(context).hasBean("mdcCorrelationScopeDecoratorBuilder"));
}
@Test
void shouldHave128BitTraceId() {
this.contextRunner.run((context) -> {
Tracing tracing = context.getBean(Tracing.class);
Span span = tracing.tracer().nextSpan();
assertThat(span.context().traceIdString()).hasSize(32);
});
}
@Test
void shouldNotSupportJoinedSpansByDefault() {
this.contextRunner.run((context) -> {
Tracing tracing = context.getBean(Tracing.class);
Span parentSpan = tracing.tracer().nextSpan();
Span childSpan = tracing.tracer().joinSpan(parentSpan.context());
assertThat(childSpan.context().traceIdString()).isEqualTo(parentSpan.context().traceIdString());
assertThat(childSpan.context().spanIdString()).isNotEqualTo(parentSpan.context().spanIdString());
assertThat(childSpan.context().parentIdString()).isEqualTo(parentSpan.context().spanIdString());
assertThat(parentSpan.context().parentIdString()).isNull();
});
}
@Test
void shouldSupportJoinedSpansIfB3UsedAndBackendSupportsIt() {
this.contextRunner
.withPropertyValues("management.tracing.propagation.type=B3",
"management.tracing.brave.span-joining-supported=true")
.run((context) -> {
Tracing tracing = context.getBean(Tracing.class);
Span parentSpan = tracing.tracer().nextSpan();
Span childSpan = tracing.tracer().joinSpan(parentSpan.context());
assertThat(childSpan.context().traceIdString()).isEqualTo(parentSpan.context().traceIdString());
assertThat(childSpan.context().spanIdString()).isEqualTo(parentSpan.context().spanIdString());
assertThat(childSpan.context().parentIdString()).isNull();
assertThat(parentSpan.context().parentIdString()).isNull();
});
}
@Test
void shouldFailIfSupportJoinedSpansIsEnabledAndW3cIsChosenAsType() {
this.contextRunner
.withPropertyValues("management.tracing.propagation.type=W3C",
"management.tracing.brave.span-joining-supported=true")
.run((context) -> assertThatException().isThrownBy(() -> context.getBean(Tracing.class))
.havingRootCause()
.isExactlyInstanceOf(IncompatibleConfigurationException.class)
.withMessage("The following configuration properties have incompatible values: "
+ "[management.tracing.propagation.type, management.tracing.brave.span-joining-supported]"));
}
@Test
void shouldFailIfSupportJoinedSpansIsEnabledAndW3cIsChosenAsConsume() {
this.contextRunner.withPropertyValues("management.tracing.propagation.produce=B3",
"management.tracing.propagation.consume=W3C", "management.tracing.brave.span-joining-supported=true")
.run((context) -> assertThatException().isThrownBy(() -> context.getBean(Tracing.class))
.havingRootCause()
.isExactlyInstanceOf(IncompatibleConfigurationException.class)
.withMessage("The following configuration properties have incompatible values: "
+ "[management.tracing.propagation.consume, management.tracing.brave.span-joining-supported]"));
}
@Test
void shouldFailIfSupportJoinedSpansIsEnabledAndW3cIsChosenAsProduce() {
this.contextRunner.withPropertyValues("management.tracing.propagation.consume=B3",
"management.tracing.propagation.produce=W3C", "management.tracing.brave.span-joining-supported=true")
.run((context) -> assertThatException().isThrownBy(() -> context.getBean(Tracing.class))
.havingRootCause()
.isExactlyInstanceOf(IncompatibleConfigurationException.class)
.withMessage("The following configuration properties have incompatible values: "
+ "[management.tracing.propagation.produce, management.tracing.brave.span-joining-supported]"));
}
@Test
@SuppressWarnings("rawtypes")
void compositeSpanHandlerShouldBeFirstSpanHandler() {
this.contextRunner.withUserConfiguration(SpanHandlerConfiguration.class).run((context) -> {
Tracing tracing = context.getBean(Tracing.class);
assertThat(tracing).extracting("tracer.spanHandler.delegate.handlers")
.asInstanceOf(InstanceOfAssertFactories.array(SpanHandler[].class))
.extracting((handler) -> (Class) handler.getClass())
.containsExactly(CompositeSpanHandler.class, AdditionalSpanHandler.class);
});
}
@Test
void compositeSpanHandlerUsesFilterPredicateAndReportersInOrder() {
this.contextRunner.withUserConfiguration(CompositeSpanHandlerComponentsConfiguration.class).run((context) -> {
CompositeSpanHandlerComponentsConfiguration components = context
.getBean(CompositeSpanHandlerComponentsConfiguration.class);
CompositeSpanHandler composite = context.getBean(CompositeSpanHandler.class);
assertThat(composite).extracting("spanFilters")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.containsExactly(components.filter1, components.filter2);
assertThat(composite).extracting("filters")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.containsExactly(components.predicate2, components.predicate1);
assertThat(composite).extracting("reporters")
.asInstanceOf(InstanceOfAssertFactories.LIST)
.containsExactly(components.reporter1, components.reporter3, components.reporter2);
});
}
@Test
void shouldDisablePropagationIfTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.tracing.enabled=false").run((context) -> {
assertThat(context).hasSingleBean(Factory.class);
Factory factory = context.getBean(Factory.class);
Propagation<String> propagation = factory.get();
assertThat(propagation.keys()).isEmpty();
});
}
@Test
void shouldConfigureTaggedFields() {
this.contextRunner.withPropertyValues("management.tracing.baggage.tag-fields=t1").run((context) -> {
BraveTracer braveTracer = context.getBean(BraveTracer.class);
assertThat(braveTracer).extracting("braveBaggageManager.tagFields")
.asInstanceOf(InstanceOfAssertFactories.list(String.class))
.containsExactly("t1");
});
}
@Test
void keysAreSetInBaggage() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.baggage.remote-fields=f1,f2")
.run((context) -> {
BraveTracer braveTracer = context.getBean(BraveTracer.class);
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
Observation observation = Observation.start("o1", observationRegistry)
.lowCardinalityKeyValue("f1", "v1")
.highCardinalityKeyValue("f2", "v2");
Map<String, String> baggage = braveTracer.getAllBaggage();
assertThat(baggage).isEmpty();
try (Scope ignore = observation.openScope()) {
baggage = braveTracer.getAllBaggage();
assertThat(baggage).containsAllEntriesOf(Map.of("f1", "v1", "f2", "v2"));
}
baggage = braveTracer.getAllBaggage();
assertThat(baggage).isEmpty();
});
}
private void injectToMap(Map<String, String> map, String key, String value) {
map.put(key, value);
}
private List<Factory> getInjectors(Factory factory) {
assertThat(factory).as("factory").isNotNull();
if (factory instanceof CompositePropagationFactory compositePropagationFactory) {
return compositePropagationFactory.getInjectors().toList();
}
Assertions.fail("Expected CompositePropagationFactory, found %s".formatted(factory.getClass()));
throw new AssertionError("Unreachable");
}
@Configuration(proxyBeanMethods = false)
static class CompositeSpanHandlerComponentsConfiguration {
private final SpanFilter filter1 = mock(SpanFilter.class);
private final SpanFilter filter2 = mock(SpanFilter.class);
private final SpanExportingPredicate predicate1 = mock(SpanExportingPredicate.class);
private final SpanExportingPredicate predicate2 = mock(SpanExportingPredicate.class);
private final SpanReporter reporter1 = mock(SpanReporter.class);
private final SpanReporter reporter2 = mock(SpanReporter.class);
private final SpanReporter reporter3 = mock(SpanReporter.class);
@Bean
@Order(1)
SpanFilter filter1() {
return this.filter1;
}
@Bean
@Order(2)
SpanFilter filter2() {
return this.filter2;
}
@Bean
@Order(2)
SpanExportingPredicate predicate1() {
return this.predicate1;
}
@Bean
@Order(1)
SpanExportingPredicate predicate2() {
return this.predicate2;
}
@Bean
@Order(1)
SpanReporter reporter1() {
return this.reporter1;
}
@Bean
@Order(3)
SpanReporter reporter2() {
return this.reporter2;
}
@Bean
@Order(2)
SpanReporter reporter3() {
return this.reporter3;
}
}
@Configuration(proxyBeanMethods = false)
static class SpanHandlerConfiguration {
@Bean
SpanHandler additionalSpanHandler() {
return new AdditionalSpanHandler();
}
static class AdditionalSpanHandler extends SpanHandler {
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomConfiguration {
@Bean
Tracing customTracing() {
return mock(Tracing.class);
}
@Bean
Tracer customTracer() {
return mock(Tracer.class);
}
@Bean
CurrentTraceContext customCurrentTraceContext() {
return mock(CurrentTraceContext.class);
}
@Bean
Factory customFactory() {
return mock(Factory.class);
}
@Bean
Sampler customSampler() {
return mock(Sampler.class);
}
@Bean
io.micrometer.tracing.Tracer customMicrometerTracer() {
return mock(io.micrometer.tracing.Tracer.class);
}
@Bean
BraveBaggageManager customBraveBaggageManager() {
return mock(BraveBaggageManager.class);
}
@Bean
CompositeSpanHandler customCompositeSpanHandler() {
return new CompositeSpanHandler(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
}
@Bean
SpanCustomizer customSpanCustomizer() {
return mock(SpanCustomizer.class);
}
@Bean
io.micrometer.tracing.SpanCustomizer customMicrometerSpanCustomizer() {
return mock(io.micrometer.tracing.SpanCustomizer.class);
}
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import brave.propagation.Propagation;
import brave.propagation.TraceContext;
import brave.propagation.TraceContextOrSamplingFlags;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link CompositePropagationFactory}.
*
* @author Moritz Halbritter
*/
class CompositePropagationFactoryTests {
@Test
void supportsJoin() {
Propagation.Factory supported = Mockito.mock(Propagation.Factory.class);
given(supported.supportsJoin()).willReturn(true);
given(supported.get()).willReturn(new DummyPropagation("a"));
Propagation.Factory unsupported = Mockito.mock(Propagation.Factory.class);
given(unsupported.supportsJoin()).willReturn(false);
given(unsupported.get()).willReturn(new DummyPropagation("a"));
CompositePropagationFactory factory = new CompositePropagationFactory(List.of(supported), List.of(unsupported));
assertThat(factory.supportsJoin()).isFalse();
}
@Test
void requires128BitTraceId() {
Propagation.Factory required = Mockito.mock(Propagation.Factory.class);
given(required.requires128BitTraceId()).willReturn(true);
given(required.get()).willReturn(new DummyPropagation("a"));
Propagation.Factory notRequired = Mockito.mock(Propagation.Factory.class);
given(notRequired.requires128BitTraceId()).willReturn(false);
given(notRequired.get()).willReturn(new DummyPropagation("a"));
CompositePropagationFactory factory = new CompositePropagationFactory(List.of(required), List.of(notRequired));
assertThat(factory.requires128BitTraceId()).isTrue();
}
@Nested
class CompositePropagationTests {
@Test
void keys() {
CompositePropagationFactory factory = new CompositePropagationFactory(List.of(field("a")),
List.of(field("b")));
Propagation<String> propagation = factory.get();
assertThat(propagation.keys()).containsExactly("a", "b");
}
@Test
void inject() {
CompositePropagationFactory factory = new CompositePropagationFactory(List.of(field("a"), field("b")),
List.of(field("c")));
Propagation<String> propagation = factory.get();
TraceContext context = context();
Map<String, String> request = new HashMap<>();
propagation.injector(new MapSetter()).inject(context, request);
assertThat(request).containsOnly(entry("a", "a-value"), entry("b", "b-value"));
}
@Test
void extractorWhenDelegateExtractsReturnsExtraction() {
CompositePropagationFactory factory = new CompositePropagationFactory(Collections.emptyList(),
List.of(field("a"), field("b")));
Propagation<String> propagation = factory.get();
Map<String, String> request = Map.of("a", "a-value", "b", "b-value");
TraceContextOrSamplingFlags context = propagation.extractor(new MapGetter()).extract(request);
assertThat(context.context().extra()).containsExactly("a");
}
@Test
void extractorWhenWhenNoExtractorMatchesReturnsEmptyContext() {
CompositePropagationFactory factory = new CompositePropagationFactory(Collections.emptyList(),
Collections.emptyList());
Propagation<String> propagation = factory.get();
Map<String, String> request = Collections.emptyMap();
TraceContextOrSamplingFlags context = propagation.extractor(new MapGetter()).extract(request);
assertThat(context.context()).isNull();
}
private static TraceContext context() {
return TraceContext.newBuilder().traceId(1).spanId(2).build();
}
private static DummyPropagation field(String field) {
return new DummyPropagation(field);
}
}
private static final class MapSetter implements Propagation.Setter<Map<String, String>, String> {
@Override
public void put(Map<String, String> request, String key, String value) {
request.put(key, value);
}
}
private static final class MapGetter implements Propagation.Getter<Map<String, String>, String> {
@Override
public String get(Map<String, String> request, String key) {
return request.get(key);
}
}
private static final class DummyPropagation extends Propagation.Factory implements Propagation<String> {
private final String field;
private DummyPropagation(String field) {
this.field = field;
}
@Override
public Propagation<String> get() {
return this;
}
@Override
public List<String> keys() {
return List.of(this.field);
}
@Override
public <R> TraceContext.Injector<R> injector(Propagation.Setter<R, String> setter) {
return (traceContext, request) -> setter.put(request, this.field, this.field + "-value");
}
@Override
public <R> TraceContext.Extractor<R> extractor(Propagation.Getter<R, String> getter) {
return (request) -> {
TraceContext context = TraceContext.newBuilder().traceId(1).spanId(2).addExtra(this.field).build();
return TraceContextOrSamplingFlags.create(context);
};
}
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.ContextKey;
import io.opentelemetry.context.propagation.TextMapGetter;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.context.propagation.TextMapSetter;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.springframework.boot.tracing.autoconfigure.TracingProperties.Propagation;
import org.springframework.boot.tracing.autoconfigure.TracingProperties.Propagation.PropagationType;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CompositeTextMapPropagator}.
*
* @author Moritz Halbritter
* @author Scott Frederick
*/
class CompositeTextMapPropagatorTests {
private ContextKeyRegistry contextKeyRegistry;
@BeforeEach
void setUp() {
this.contextKeyRegistry = new ContextKeyRegistry();
}
@Test
void collectsAllFields() {
CompositeTextMapPropagator propagator = new CompositeTextMapPropagator(List.of(field("a")), List.of(field("b")),
field("c"));
assertThat(propagator.fields()).containsExactly("a", "b", "c");
}
@Test
void injectAllFields() {
CompositeTextMapPropagator propagator = new CompositeTextMapPropagator(List.of(field("a"), field("b")),
Collections.emptyList(), null);
TextMapSetter<Object> setter = setter();
Object carrier = carrier();
propagator.inject(context(), carrier, setter);
InOrder inOrder = Mockito.inOrder(setter);
inOrder.verify(setter).set(carrier, "a", "a-value");
inOrder.verify(setter).set(carrier, "b", "b-value");
}
@Test
void extractWithoutBaggagePropagator() {
CompositeTextMapPropagator propagator = new CompositeTextMapPropagator(Collections.emptyList(),
List.of(field("a"), field("b")), null);
Context context = context();
Map<String, String> carrier = Map.of("a", "a-value", "b", "b-value");
context = propagator.extract(context, carrier, new MapTextMapGetter());
Object a = context.get(getObjectContextKey("a"));
assertThat(a).isEqualTo("a-value");
Object b = context.get(getObjectContextKey("b"));
assertThat(b).isNull();
}
@Test
void extractWithBaggagePropagator() {
CompositeTextMapPropagator propagator = new CompositeTextMapPropagator(Collections.emptyList(),
List.of(field("a"), field("b")), field("c"));
Context context = context();
Map<String, String> carrier = Map.of("a", "a-value", "b", "b-value", "c", "c-value");
context = propagator.extract(context, carrier, new MapTextMapGetter());
Object c = context.get(getObjectContextKey("c"));
assertThat(c).isEqualTo("c-value");
}
@Test
void createMapsInjectorsAndExtractors() {
Propagation properties = new Propagation();
properties.setProduce(List.of(PropagationType.W3C));
properties.setConsume(List.of(PropagationType.B3));
CompositeTextMapPropagator propagator = (CompositeTextMapPropagator) CompositeTextMapPropagator
.create(properties, null);
assertThat(propagator.getInjectors()).hasExactlyElementsOfTypes(W3CTraceContextPropagator.class);
assertThat(propagator.getExtractors()).hasExactlyElementsOfTypes(B3Propagator.class);
}
private DummyTextMapPropagator field(String field) {
return new DummyTextMapPropagator(field, this.contextKeyRegistry);
}
private ContextKey<Object> getObjectContextKey(String name) {
return this.contextKeyRegistry.get(name);
}
@SuppressWarnings("unchecked")
private static <T> TextMapSetter<T> setter() {
return Mockito.mock(TextMapSetter.class);
}
private static Object carrier() {
return new Object();
}
private static Context context() {
return Context.current();
}
private static final class ContextKeyRegistry {
private final Map<String, ContextKey<Object>> contextKeys = new HashMap<>();
private ContextKey<Object> get(String name) {
return this.contextKeys.computeIfAbsent(name, (ignore) -> ContextKey.named(name));
}
}
private static final class MapTextMapGetter implements TextMapGetter<Map<String, String>> {
@Override
public Iterable<String> keys(Map<String, String> carrier) {
return carrier.keySet();
}
@Override
public String get(Map<String, String> carrier, String key) {
if (carrier == null) {
return null;
}
return carrier.get(key);
}
}
private static final class DummyTextMapPropagator implements TextMapPropagator {
private final String field;
private final ContextKeyRegistry contextKeyRegistry;
private DummyTextMapPropagator(String field, ContextKeyRegistry contextKeyRegistry) {
this.field = field;
this.contextKeyRegistry = contextKeyRegistry;
}
@Override
public Collection<String> fields() {
return List.of(this.field);
}
@Override
public <C> void inject(Context context, C carrier, TextMapSetter<C> setter) {
setter.set(carrier, this.field, this.field + "-value");
}
@Override
public <C> Context extract(Context context, C carrier, TextMapGetter<C> getter) {
String value = getter.get(carrier, this.field);
if (value != null) {
return context.with(this.contextKeyRegistry.get(this.field), value);
}
return context;
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import brave.baggage.BaggageField;
import brave.baggage.BaggagePropagation;
import brave.baggage.BaggagePropagation.FactoryBuilder;
import brave.baggage.BaggagePropagationConfig;
import brave.propagation.Propagation;
import brave.propagation.Propagation.Factory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LocalBaggageFields}.
*
* @author Moritz Halbritter
*/
class LocalBaggageFieldsTests {
@Test
void extractFromBuilder() {
FactoryBuilder builder = createBuilder();
builder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create("remote-field-1")));
builder.add(BaggagePropagationConfig.SingleBaggageField.remote(BaggageField.create("remote-field-2")));
builder.add(BaggagePropagationConfig.SingleBaggageField.local(BaggageField.create("local-field-1")));
builder.add(BaggagePropagationConfig.SingleBaggageField.local(BaggageField.create("local-field-2")));
LocalBaggageFields fields = LocalBaggageFields.extractFrom(builder);
assertThat(fields.asList()).containsExactlyInAnyOrder("local-field-1", "local-field-2");
}
@Test
void empty() {
assertThat(LocalBaggageFields.empty().asList()).isEmpty();
}
private static FactoryBuilder createBuilder() {
return BaggagePropagation.newFactoryBuilder(new Factory() {
@Override
public Propagation<String> get() {
return null;
}
});
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LogCorrelationEnvironmentPostProcessor}.
*
* @author Jonatan Ivanov
* @author Phillip Webb
*/
class LogCorrelationEnvironmentPostProcessorTests {
private final ConfigurableEnvironment environment = new StandardEnvironment();
private final SpringApplication application = new SpringApplication();
private final LogCorrelationEnvironmentPostProcessor postProcessor = new LogCorrelationEnvironmentPostProcessor();
@Test
void getExpectCorrelationIdPropertyWhenMicrometerTracingPresentReturnsTrue() {
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false))
.isTrue();
}
@Test
@ClassPathExclusions("micrometer-tracing-*.jar")
void getExpectCorrelationIdPropertyWhenMicrometerTracingMissingReturnsFalse() {
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false))
.isFalse();
}
@Test
void getExpectCorrelationIdPropertyWhenTracingDisabledReturnsFalse() {
TestPropertyValues.of("management.tracing.enabled=false").applyTo(this.environment);
this.postProcessor.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY, Boolean.class, false))
.isFalse();
}
@Test
void postProcessEnvironmentAddsEnumerablePropertySource() {
this.postProcessor.postProcessEnvironment(this.environment, this.application);
PropertySource<?> propertySource = this.environment.getPropertySources().get("logCorrelation");
assertThat(propertySource).isInstanceOf(EnumerablePropertySource.class);
assertThat(((EnumerablePropertySource<?>) propertySource).getPropertyNames())
.containsExactly(LoggingSystem.EXPECT_CORRELATION_ID_PROPERTY);
}
}

View File

@@ -0,0 +1,282 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import io.micrometer.common.annotation.ValueExpressionResolver;
import io.micrometer.common.annotation.ValueResolver;
import io.micrometer.core.instrument.observation.MeterObservationHandler;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.annotation.DefaultNewSpanParser;
import io.micrometer.tracing.annotation.ImperativeMethodInvocationProcessor;
import io.micrometer.tracing.annotation.MethodInvocationProcessor;
import io.micrometer.tracing.annotation.NewSpanParser;
import io.micrometer.tracing.annotation.SpanAspect;
import io.micrometer.tracing.annotation.SpanTagAnnotationHandler;
import io.micrometer.tracing.handler.DefaultTracingObservationHandler;
import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler;
import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler;
import io.micrometer.tracing.handler.TracingObservationHandler;
import io.micrometer.tracing.propagation.Propagator;
import org.aspectj.weaver.Advice;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MicrometerTracingAutoConfiguration}.
*
* @author Moritz Halbritter
* @author Jonatan Ivanov
* @author Brian Clozel
*/
class MicrometerTracingAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.observations.annotations.enabled=true")
.withConfiguration(AutoConfigurations.of(MicrometerTracingAutoConfiguration.class));
@Test
void shouldSupplyBeans() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class, PropagatorConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(DefaultTracingObservationHandler.class);
assertThat(context).hasSingleBean(PropagatingReceiverTracingObservationHandler.class);
assertThat(context).hasSingleBean(PropagatingSenderTracingObservationHandler.class);
assertThat(context).hasSingleBean(DefaultNewSpanParser.class);
assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class);
assertThat(context).hasSingleBean(SpanAspect.class);
assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class);
});
}
@Test
@SuppressWarnings("rawtypes")
void shouldSupplyBeansInCorrectOrder() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class, PropagatorConfiguration.class)
.run((context) -> {
List<TracingObservationHandler> tracingObservationHandlers = context
.getBeanProvider(TracingObservationHandler.class)
.orderedStream()
.toList();
assertThat(tracingObservationHandlers).hasSize(3);
assertThat(tracingObservationHandlers.get(0))
.isInstanceOf(PropagatingReceiverTracingObservationHandler.class);
assertThat(tracingObservationHandlers.get(1))
.isInstanceOf(PropagatingSenderTracingObservationHandler.class);
assertThat(tracingObservationHandlers.get(2)).isInstanceOf(DefaultTracingObservationHandler.class);
});
}
@Test
void shouldBackOffOnCustomBeans() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class, CustomConfiguration.class)
.run((context) -> {
assertThat(context).hasBean("customDefaultTracingObservationHandler");
assertThat(context).hasSingleBean(DefaultTracingObservationHandler.class);
assertThat(context).hasBean("customPropagatingReceiverTracingObservationHandler");
assertThat(context).hasSingleBean(PropagatingReceiverTracingObservationHandler.class);
assertThat(context).hasBean("customPropagatingSenderTracingObservationHandler");
assertThat(context).hasSingleBean(PropagatingSenderTracingObservationHandler.class);
assertThat(context).hasBean("customDefaultNewSpanParser");
assertThat(context).hasSingleBean(DefaultNewSpanParser.class);
assertThat(context).hasBean("customImperativeMethodInvocationProcessor");
assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class);
assertThat(context).hasBean("customSpanAspect");
assertThat(context).hasSingleBean(SpanAspect.class);
assertThat(context).hasBean("customSpanTagAnnotationHandler");
assertThat(context).hasSingleBean(SpanTagAnnotationHandler.class);
});
}
@Test
void shouldNotSupplyBeansIfMicrometerIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer")).run((context) -> {
assertThat(context).doesNotHaveBean(DefaultTracingObservationHandler.class);
assertThat(context).doesNotHaveBean(PropagatingReceiverTracingObservationHandler.class);
assertThat(context).doesNotHaveBean(PropagatingSenderTracingObservationHandler.class);
assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class);
assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class);
assertThat(context).doesNotHaveBean(SpanAspect.class);
});
}
@Test
void shouldNotSupplyBeansIfTracerIsMissing() {
this.contextRunner.withUserConfiguration(PropagatorConfiguration.class).run((context) -> {
assertThat(context).doesNotHaveBean(DefaultTracingObservationHandler.class);
assertThat(context).doesNotHaveBean(PropagatingReceiverTracingObservationHandler.class);
assertThat(context).doesNotHaveBean(PropagatingSenderTracingObservationHandler.class);
assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class);
assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class);
assertThat(context).doesNotHaveBean(SpanAspect.class);
});
}
@Test
void shouldNotSupplyAspectBeansIfPropertyIsDisabled() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class, PropagatorConfiguration.class)
.withPropertyValues("management.observations.annotations.enabled=false")
.run((context) -> {
assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class);
assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class);
assertThat(context).doesNotHaveBean(SpanAspect.class);
});
}
@Test
void shouldNotSupplyBeansIfAspectjIsMissing() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withClassLoader(new FilteredClassLoader(Advice.class))
.run((context) -> {
assertThat(context).doesNotHaveBean(DefaultNewSpanParser.class);
assertThat(context).doesNotHaveBean(ImperativeMethodInvocationProcessor.class);
assertThat(context).doesNotHaveBean(SpanAspect.class);
});
}
@Test
void shouldNotSupplyBeansIfPropagatorIsMissing() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class).run((context) -> {
assertThat(context).doesNotHaveBean(PropagatingSenderTracingObservationHandler.class);
assertThat(context).doesNotHaveBean(PropagatingReceiverTracingObservationHandler.class);
assertThat(context).hasSingleBean(DefaultNewSpanParser.class);
assertThat(context).hasSingleBean(ImperativeMethodInvocationProcessor.class);
assertThat(context).hasSingleBean(SpanAspect.class);
});
}
@Test
void shouldConfigureSpanTagAnnotationHandler() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class, SpanTagAnnotationHandlerConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(DefaultNewSpanParser.class);
assertThat(context).hasSingleBean(SpanAspect.class);
assertThat(context.getBean(ImperativeMethodInvocationProcessor.class)).hasFieldOrPropertyWithValue(
"spanTagAnnotationHandler", context.getBean(SpanTagAnnotationHandler.class));
});
}
@Test
void shouldCreateTracingAndMeterObservationHandlerGroupWhenHasTracing() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(ObservationHandlerGroup.class);
ObservationHandlerGroup group = context.getBean(ObservationHandlerGroup.class);
assertThat(group).isInstanceOf(TracingAndMeterObservationHandlerGroup.class);
assertThat(group.isMember(mock(ObservationHandler.class))).isFalse();
assertThat(group.isMember(mock(TracingObservationHandler.class))).isTrue();
assertThat(group.isMember(mock(MeterObservationHandler.class))).isTrue();
});
}
@Test
void shouldCreateTracingObservationHandlerGroupWhenMetricsIsNotOnClassPath() {
this.contextRunner.withUserConfiguration(TracerConfiguration.class)
.withClassLoader(new FilteredClassLoader("io.micrometer.core"))
.run((context) -> {
assertThat(context).hasSingleBean(ObservationHandlerGroup.class);
ObservationHandlerGroup group = context.getBean(ObservationHandlerGroup.class);
assertThat(group).isNotInstanceOf(TracingAndMeterObservationHandlerGroup.class);
assertThat(group.isMember(mock(ObservationHandler.class))).isFalse();
assertThat(group.isMember(mock(TracingObservationHandler.class))).isTrue();
});
}
@Configuration(proxyBeanMethods = false)
private static final class TracerConfiguration {
@Bean
Tracer tracer() {
return mock(Tracer.class);
}
}
@Configuration(proxyBeanMethods = false)
private static final class PropagatorConfiguration {
@Bean
Propagator propagator() {
return mock(Propagator.class);
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomConfiguration {
@Bean
DefaultTracingObservationHandler customDefaultTracingObservationHandler() {
return mock(DefaultTracingObservationHandler.class);
}
@Bean
PropagatingReceiverTracingObservationHandler<?> customPropagatingReceiverTracingObservationHandler() {
return mock(PropagatingReceiverTracingObservationHandler.class);
}
@Bean
PropagatingSenderTracingObservationHandler<?> customPropagatingSenderTracingObservationHandler() {
return mock(PropagatingSenderTracingObservationHandler.class);
}
@Bean
DefaultNewSpanParser customDefaultNewSpanParser() {
return new DefaultNewSpanParser();
}
@Bean
ImperativeMethodInvocationProcessor customImperativeMethodInvocationProcessor(NewSpanParser newSpanParser,
Tracer tracer) {
return new ImperativeMethodInvocationProcessor(newSpanParser, tracer);
}
@Bean
SpanAspect customSpanAspect(MethodInvocationProcessor methodInvocationProcessor) {
return new SpanAspect(methodInvocationProcessor);
}
@Bean
SpanTagAnnotationHandler customSpanTagAnnotationHandler() {
return new SpanTagAnnotationHandler((aClass) -> mock(ValueResolver.class),
(aClass) -> mock(ValueExpressionResolver.class));
}
}
@Configuration(proxyBeanMethods = false)
private static final class SpanTagAnnotationHandlerConfiguration {
@Bean
SpanTagAnnotationHandler spanTagAnnotationHandler() {
return new SpanTagAnnotationHandler((valueResolverClass) -> null, (valueExpressionResolverClass) -> null);
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NoopTracerAutoConfiguration}.
*
* @author Moritz Halbritter
*/
class NoopTracerAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(NoopTracerAutoConfiguration.class));
@Test
void shouldSupplyNoopTracer() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(Tracer.class);
Tracer tracer = context.getBean(Tracer.class);
assertThat(tracer).isEqualTo(Tracer.NOOP);
});
}
@Test
void shouldBackOffOnCustomTracer() {
this.contextRunner.withUserConfiguration(CustomTracerConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(Tracer.class);
assertThat(context).hasBean("customTracer");
Tracer tracer = context.getBean(Tracer.class);
assertThat(tracer).isNotEqualTo(Tracer.NOOP);
});
}
@Test
void shouldBackOffIfMicrometerTracingIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing"))
.run((context) -> assertThat(context).doesNotHaveBean(Tracer.class));
}
@Configuration(proxyBeanMethods = false)
private static final class CustomTracerConfiguration {
@Bean
Tracer customTracer() {
return mock(Tracer.class);
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OnEnabledTracingCondition}.
*
* @author Moritz Halbritter
*/
class OnEnabledTracingConditionTests {
@Test
void shouldMatchIfNoPropertyIsSet() {
OnEnabledTracingCondition condition = new OnEnabledTracingCondition();
ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext(), mockMetadata(""));
assertThat(outcome.isMatch()).isTrue();
assertThat(outcome.getMessage()).isEqualTo("@ConditionalOnEnabledTracing tracing is enabled by default");
}
@Test
void shouldNotMatchIfGlobalPropertyIsFalse() {
OnEnabledTracingCondition condition = new OnEnabledTracingCondition();
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(Map.of("management.tracing.enabled", "false")), mockMetadata(""));
assertThat(outcome.isMatch()).isFalse();
assertThat(outcome.getMessage()).isEqualTo("@ConditionalOnEnabledTracing management.tracing.enabled is false");
}
@Test
void shouldMatchIfGlobalPropertyIsTrue() {
OnEnabledTracingCondition condition = new OnEnabledTracingCondition();
ConditionOutcome outcome = condition
.getMatchOutcome(mockConditionContext(Map.of("management.tracing.enabled", "true")), mockMetadata(""));
assertThat(outcome.isMatch()).isTrue();
assertThat(outcome.getMessage()).isEqualTo("@ConditionalOnEnabledTracing management.tracing.enabled is true");
}
@Test
void shouldNotMatchIfExporterPropertyIsFalse() {
OnEnabledTracingCondition condition = new OnEnabledTracingCondition();
ConditionOutcome outcome = condition.getMatchOutcome(
mockConditionContext(Map.of("management.zipkin.tracing.export.enabled", "false")),
mockMetadata("zipkin"));
assertThat(outcome.isMatch()).isFalse();
assertThat(outcome.getMessage())
.isEqualTo("@ConditionalOnEnabledTracing management.zipkin.tracing.export.enabled is false");
}
@Test
void shouldMatchIfExporterPropertyIsTrue() {
OnEnabledTracingCondition condition = new OnEnabledTracingCondition();
ConditionOutcome outcome = condition.getMatchOutcome(
mockConditionContext(Map.of("management.zipkin.tracing.export.enabled", "true")),
mockMetadata("zipkin"));
assertThat(outcome.isMatch()).isTrue();
assertThat(outcome.getMessage())
.isEqualTo("@ConditionalOnEnabledTracing management.zipkin.tracing.export.enabled is true");
}
@Test
void exporterPropertyShouldOverrideGlobalPropertyIfTrue() {
OnEnabledTracingCondition condition = new OnEnabledTracingCondition();
ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext(
Map.of("management.tracing.enabled", "false", "management.zipkin.tracing.export.enabled", "true")),
mockMetadata("zipkin"));
assertThat(outcome.isMatch()).isTrue();
assertThat(outcome.getMessage())
.isEqualTo("@ConditionalOnEnabledTracing management.zipkin.tracing.export.enabled is true");
}
@Test
void exporterPropertyShouldOverrideGlobalPropertyIfFalse() {
OnEnabledTracingCondition condition = new OnEnabledTracingCondition();
ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext(
Map.of("management.tracing.enabled", "true", "management.zipkin.tracing.export.enabled", "false")),
mockMetadata("zipkin"));
assertThat(outcome.isMatch()).isFalse();
assertThat(outcome.getMessage())
.isEqualTo("@ConditionalOnEnabledTracing management.zipkin.tracing.export.enabled is false");
}
private ConditionContext mockConditionContext() {
return mockConditionContext(Collections.emptyMap());
}
private ConditionContext mockConditionContext(Map<String, String> properties) {
ConditionContext context = mock(ConditionContext.class);
MockEnvironment environment = new MockEnvironment();
properties.forEach(environment::setProperty);
given(context.getEnvironment()).willReturn(environment);
return context;
}
private AnnotatedTypeMetadata mockMetadata(String exporter) {
AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class);
given(metadata.getAnnotationAttributes(ConditionalOnEnabledTracing.class.getName()))
.willReturn(Map.of("value", exporter));
return metadata;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.lang.reflect.Method;
import java.util.List;
import java.util.function.Function;
import io.opentelemetry.context.ContextStorage;
import org.junit.jupiter.api.Test;
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
import org.springframework.boot.tracing.autoconfigure.OpenTelemetryEventPublisherBeansApplicationListener.Wrapper.Storage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Integration tests for {@link OpenTelemetryEventPublisherBeansTestExecutionListener}.
*
* @author Phillip Webb
*/
@ForkedClassPath
class OpenTelemetryEventPublishingContextWrapperBeansTestExecutionListenerIntegrationTests {
private final ContextStorage parent = mock(ContextStorage.class);
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
void wrapperIsInstalled() throws Exception {
Class<?> wrappersClass = Class.forName("io.opentelemetry.context.ContextStorageWrappers");
Method getWrappersMethod = wrappersClass.getDeclaredMethod("getWrappers");
getWrappersMethod.setAccessible(true);
List<Function> wrappers = (List<Function>) getWrappersMethod.invoke(null);
assertThat(wrappers).anyMatch((function) -> function.apply(this.parent) instanceof Storage);
}
}

View File

@@ -0,0 +1,619 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import io.micrometer.tracing.SpanCustomizer;
import io.micrometer.tracing.Tracer.SpanInScope;
import io.micrometer.tracing.otel.bridge.EventListener;
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
import io.micrometer.tracing.otel.bridge.OtelPropagator;
import io.micrometer.tracing.otel.bridge.OtelSpanCustomizer;
import io.micrometer.tracing.otel.bridge.OtelTracer;
import io.micrometer.tracing.otel.bridge.OtelTracer.EventPublisher;
import io.micrometer.tracing.otel.bridge.Slf4JBaggageEventListener;
import io.micrometer.tracing.otel.bridge.Slf4JEventListener;
import io.micrometer.tracing.otel.propagation.BaggageTextMapPropagator;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.extension.trace.propagation.B3Propagator;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.SpanLimits;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.data.SpanData;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.observation.autoconfigure.ObservationAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenTelemetryTracingAutoConfiguration}.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Yanming Zhou
*/
class OpenTelemetryTracingAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration.class,
OpenTelemetryTracingAutoConfiguration.class));
@Test
void shouldSupplyBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(OtelTracer.class);
assertThat(context).hasSingleBean(EventPublisher.class);
assertThat(context).hasSingleBean(OtelCurrentTraceContext.class);
assertThat(context).hasSingleBean(SdkTracerProvider.class);
assertThat(context).hasSingleBean(ContextPropagators.class);
assertThat(context).hasSingleBean(Sampler.class);
assertThat(context).hasSingleBean(Tracer.class);
assertThat(context).hasSingleBean(Slf4JEventListener.class);
assertThat(context).hasSingleBean(Slf4JBaggageEventListener.class);
assertThat(context).hasSingleBean(SpanProcessor.class);
assertThat(context).hasSingleBean(OtelPropagator.class);
assertThat(context).hasSingleBean(TextMapPropagator.class);
assertThat(context).hasSingleBean(OtelSpanCustomizer.class);
assertThat(context).hasSingleBean(SpanProcessors.class);
assertThat(context).hasSingleBean(SpanExporters.class);
});
}
@Test
void samplerIsParentBased() {
this.contextRunner.run((context) -> {
Sampler sampler = context.getBean(Sampler.class);
assertThat(sampler).isNotNull();
assertThat(sampler.getDescription()).startsWith("ParentBased{");
});
}
@ParameterizedTest
@ValueSource(strings = { "io.micrometer.tracing.otel", "io.opentelemetry.sdk", "io.opentelemetry.api" })
void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) {
this.contextRunner.withClassLoader(new FilteredClassLoader(packageName)).run((context) -> {
assertThat(context).doesNotHaveBean(OtelTracer.class);
assertThat(context).doesNotHaveBean(EventPublisher.class);
assertThat(context).doesNotHaveBean(OtelCurrentTraceContext.class);
assertThat(context).doesNotHaveBean(SdkTracerProvider.class);
assertThat(context).doesNotHaveBean(ContextPropagators.class);
assertThat(context).doesNotHaveBean(Sampler.class);
assertThat(context).doesNotHaveBean(Tracer.class);
assertThat(context).doesNotHaveBean(Slf4JEventListener.class);
assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class);
assertThat(context).doesNotHaveBean(SpanProcessor.class);
assertThat(context).doesNotHaveBean(OtelPropagator.class);
assertThat(context).doesNotHaveBean(TextMapPropagator.class);
assertThat(context).doesNotHaveBean(OtelSpanCustomizer.class);
assertThat(context).doesNotHaveBean(SpanProcessors.class);
assertThat(context).doesNotHaveBean(SpanExporters.class);
});
}
@Test
void shouldBackOffOnCustomBeans() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> {
assertThat(context).hasBean("customMicrometerTracer");
assertThat(context).hasSingleBean(io.micrometer.tracing.Tracer.class);
assertThat(context).hasBean("customEventPublisher");
assertThat(context).hasSingleBean(EventPublisher.class);
assertThat(context).hasBean("customOtelCurrentTraceContext");
assertThat(context).hasSingleBean(OtelCurrentTraceContext.class);
assertThat(context).hasBean("customSdkTracerProvider");
assertThat(context).hasSingleBean(SdkTracerProvider.class);
assertThat(context).hasBean("customContextPropagators");
assertThat(context).hasSingleBean(ContextPropagators.class);
assertThat(context).hasBean("customSampler");
assertThat(context).hasSingleBean(Sampler.class);
assertThat(context).hasBean("customTracer");
assertThat(context).hasSingleBean(Tracer.class);
assertThat(context).hasBean("customSlf4jEventListener");
assertThat(context).hasSingleBean(Slf4JEventListener.class);
assertThat(context).hasBean("customSlf4jBaggageEventListener");
assertThat(context).hasSingleBean(Slf4JBaggageEventListener.class);
assertThat(context).hasBean("customOtelPropagator");
assertThat(context).hasSingleBean(OtelPropagator.class);
assertThat(context).hasBean("customSpanCustomizer");
assertThat(context).hasSingleBean(SpanCustomizer.class);
assertThat(context).hasBean("customSpanProcessors");
assertThat(context).hasSingleBean(SpanProcessors.class);
assertThat(context).hasBean("customSpanExporters");
assertThat(context).hasSingleBean(SpanExporters.class);
assertThat(context).hasBean("customBatchSpanProcessor");
assertThat(context).hasSingleBean(BatchSpanProcessor.class);
});
}
@Test
void shouldSetupDefaultResourceAttributes() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(ObservationAutoConfiguration.class, MicrometerTracingAutoConfiguration.class))
.withUserConfiguration(InMemoryRecordingSpanExporterConfiguration.class)
.withPropertyValues("management.tracing.sampling.probability=1.0")
.run((context) -> {
context.getBean(io.micrometer.tracing.Tracer.class).nextSpan().name("test").end();
InMemoryRecordingSpanExporter exporter = context.getBean(InMemoryRecordingSpanExporter.class);
exporter.await(Duration.ofSeconds(10));
SpanData spanData = exporter.getExportedSpans().get(0);
Map<AttributeKey<?>, Object> expectedAttributes = Resource.getDefault()
.merge(Resource.create(Attributes.of(AttributeKey.stringKey("service.name"), "unknown_service")))
.getAttributes()
.asMap();
assertThat(spanData.getResource().getAttributes().asMap()).isEqualTo(expectedAttributes);
});
}
@Test
void shouldAllowMultipleSpanProcessors() {
this.contextRunner.withUserConfiguration(AdditionalSpanProcessorConfiguration.class).run((context) -> {
assertThat(context.getBeansOfType(SpanProcessor.class)).hasSize(2);
assertThat(context).hasBean("customSpanProcessor");
SpanProcessors spanProcessors = context.getBean(SpanProcessors.class);
assertThat(spanProcessors).hasSize(2);
});
}
@Test
void shouldAllowMultipleSpanExporters() {
this.contextRunner.withUserConfiguration(MultipleSpanExporterConfiguration.class).run((context) -> {
assertThat(context.getBeansOfType(SpanExporter.class)).hasSize(2);
assertThat(context).hasBean("spanExporter1");
assertThat(context).hasBean("spanExporter2");
SpanExporters spanExporters = context.getBean(SpanExporters.class);
assertThat(spanExporters).hasSize(2);
});
}
@Test
void shouldAllowMultipleTextMapPropagators() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> {
assertThat(context.getBeansOfType(TextMapPropagator.class)).hasSize(2);
assertThat(context).hasBean("customTextMapPropagator");
});
}
@Test
void shouldNotSupplySlf4jBaggageEventListenerWhenBaggageCorrelationDisabled() {
this.contextRunner.withPropertyValues("management.tracing.baggage.correlation.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class));
}
@Test
void shouldNotSupplySlf4JBaggageEventListenerWhenBaggageDisabled() {
this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(Slf4JBaggageEventListener.class));
}
@Test
void shouldSupplyB3PropagationIfPropagationPropertySet() {
this.contextRunner.withPropertyValues("management.tracing.propagation.type=B3").run((context) -> {
TextMapPropagator propagator = context.getBean(TextMapPropagator.class);
List<TextMapPropagator> injectors = getInjectors(propagator);
assertThat(injectors).hasExactlyElementsOfTypes(B3Propagator.class, BaggageTextMapPropagator.class);
});
}
@Test
void shouldSupplyB3PropagationIfPropagationPropertySetAndBaggageDisabled() {
this.contextRunner
.withPropertyValues("management.tracing.propagation.type=B3", "management.tracing.baggage.enabled=false")
.run((context) -> {
TextMapPropagator propagator = context.getBean(TextMapPropagator.class);
List<TextMapPropagator> injectors = getInjectors(propagator);
assertThat(injectors).hasExactlyElementsOfTypes(B3Propagator.class);
});
}
@Test
void shouldSupplyW3CPropagationWithBaggageByDefault() {
this.contextRunner.withPropertyValues("management.tracing.baggage.remote-fields=foo").run((context) -> {
TextMapPropagator propagator = context.getBean(TextMapPropagator.class);
List<TextMapPropagator> injectors = getInjectors(propagator);
List<String> fields = new ArrayList<>();
for (TextMapPropagator injector : injectors) {
fields.addAll(injector.fields());
}
assertThat(fields).containsExactly("traceparent", "tracestate", "baggage", "foo");
});
}
@Test
void shouldSupplyW3CPropagationWithoutBaggageWhenDisabled() {
this.contextRunner.withPropertyValues("management.tracing.baggage.enabled=false").run((context) -> {
TextMapPropagator propagator = context.getBean(TextMapPropagator.class);
List<TextMapPropagator> injectors = getInjectors(propagator);
assertThat(injectors).hasExactlyElementsOfTypes(W3CTraceContextPropagator.class);
});
}
@Test
void shouldConfigureRemoteAndTaggedFields() {
this.contextRunner
.withPropertyValues("management.tracing.baggage.remote-fields=r1",
"management.tracing.baggage.tag-fields=t1")
.run((context) -> {
CompositeTextMapPropagator propagator = context.getBean(CompositeTextMapPropagator.class);
assertThat(propagator).extracting("baggagePropagator.baggageManager.remoteFields")
.asInstanceOf(InstanceOfAssertFactories.list(String.class))
.containsExactly("r1");
assertThat(propagator).extracting("baggagePropagator.baggageManager.tagFields")
.asInstanceOf(InstanceOfAssertFactories.list(String.class))
.containsExactly("t1");
});
}
@Test
void shouldCustomizeSdkTracerProvider() {
this.contextRunner.withUserConfiguration(SdkTracerProviderCustomizationConfiguration.class).run((context) -> {
SdkTracerProvider tracerProvider = context.getBean(SdkTracerProvider.class);
assertThat(tracerProvider.getSpanLimits().getMaxNumberOfEvents()).isEqualTo(42);
assertThat(tracerProvider.getSampler()).isEqualTo(Sampler.alwaysOn());
});
}
@Test
void defaultSpanProcessorShouldUseMeterProviderIfAvailable() {
this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class).run((context) -> {
MeterProvider meterProvider = context.getBean(MeterProvider.class);
assertThat(Mockito.mockingDetails(meterProvider).isMock()).isTrue();
then(meterProvider).should().meterBuilder(anyString());
});
}
@Test
void shouldDisablePropagationIfTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.tracing.enabled=false").run((context) -> {
assertThat(context).hasSingleBean(TextMapPropagator.class);
TextMapPropagator propagator = context.getBean(TextMapPropagator.class);
assertThat(propagator.fields()).isEmpty();
});
}
@Test
void batchSpanProcessorShouldBeConfiguredWithCustomProperties() {
this.contextRunner
.withPropertyValues("management.tracing.opentelemetry.export.timeout=45s",
"management.tracing.opentelemetry.export.include-unsampled=true",
"management.tracing.opentelemetry.export.max-batch-size=256",
"management.tracing.opentelemetry.export.max-queue-size=4096",
"management.tracing.opentelemetry.export.schedule-delay=15s")
.run((context) -> {
assertThat(context).hasSingleBean(BatchSpanProcessor.class);
BatchSpanProcessor batchSpanProcessor = context.getBean(BatchSpanProcessor.class);
assertThat(batchSpanProcessor).hasFieldOrPropertyWithValue("exportUnsampledSpans", true)
.extracting("worker")
.hasFieldOrPropertyWithValue("exporterTimeoutNanos", Duration.ofSeconds(45).toNanos())
.hasFieldOrPropertyWithValue("maxExportBatchSize", 256)
.hasFieldOrPropertyWithValue("scheduleDelayNanos", Duration.ofSeconds(15).toNanos())
.extracting("queue")
.satisfies((queue) -> assertThat(ReflectionTestUtils.<Integer>invokeMethod(queue, "capacity"))
.isEqualTo(4096));
});
}
@Test
void batchSpanProcessorShouldBeConfiguredWithDefaultProperties() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(BatchSpanProcessor.class);
BatchSpanProcessor batchSpanProcessor = context.getBean(BatchSpanProcessor.class);
assertThat(batchSpanProcessor).hasFieldOrPropertyWithValue("exportUnsampledSpans", false)
.extracting("worker")
.hasFieldOrPropertyWithValue("exporterTimeoutNanos", Duration.ofSeconds(30).toNanos())
.hasFieldOrPropertyWithValue("maxExportBatchSize", 512)
.hasFieldOrPropertyWithValue("scheduleDelayNanos", Duration.ofSeconds(5).toNanos())
.extracting("queue")
.satisfies((queue) -> assertThat(ReflectionTestUtils.<Integer>invokeMethod(queue, "capacity"))
.isEqualTo(2048));
});
}
@Test // gh-41439
@ForkedClassPath
void shouldPublishEventsWhenContextStorageIsInitializedEarly() {
this.contextRunner.withInitializer(this::initializeOpenTelemetry)
.withUserConfiguration(OtelEventListener.class)
.run((context) -> {
OtelEventListener listener = context.getBean(OtelEventListener.class);
io.micrometer.tracing.Tracer micrometerTracer = context.getBean(io.micrometer.tracing.Tracer.class);
io.micrometer.tracing.Span span = micrometerTracer.nextSpan().name("test");
try (SpanInScope scoped = micrometerTracer.withSpan(span.start())) {
assertThat(listener.events).isNotEmpty();
}
finally {
span.end();
}
});
}
private void initializeOpenTelemetry(ConfigurableApplicationContext context) {
context.addApplicationListener(new OpenTelemetryEventPublisherBeansApplicationListener());
Span.current();
}
private List<TextMapPropagator> getInjectors(TextMapPropagator propagator) {
assertThat(propagator).as("propagator").isNotNull();
if (propagator instanceof CompositeTextMapPropagator compositePropagator) {
return compositePropagator.getInjectors().stream().toList();
}
fail("Expected CompositeTextMapPropagator, found %s".formatted(propagator.getClass()));
throw new AssertionError("Unreachable");
}
@Configuration(proxyBeanMethods = false)
private static final class MeterProviderConfiguration {
@Bean
MeterProvider meterProvider() {
MeterProvider mock = mock(MeterProvider.class);
given(mock.meterBuilder(anyString()))
.willAnswer((invocation) -> MeterProvider.noop().meterBuilder(invocation.getArgument(0, String.class)));
return mock;
}
}
@Configuration(proxyBeanMethods = false)
private static final class AdditionalSpanProcessorConfiguration {
@Bean
SpanProcessor customSpanProcessor() {
return mock(SpanProcessor.class);
}
}
@Configuration(proxyBeanMethods = false)
private static final class MultipleSpanExporterConfiguration {
@Bean
SpanExporter spanExporter1() {
return new DummySpanExporter();
}
@Bean
SpanExporter spanExporter2() {
return new DummySpanExporter();
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomConfiguration {
@Bean
BatchSpanProcessor customBatchSpanProcessor() {
return mock(BatchSpanProcessor.class);
}
@Bean
SpanProcessors customSpanProcessors() {
return SpanProcessors.of(mock(SpanProcessor.class));
}
@Bean
SpanExporters customSpanExporters() {
return SpanExporters.of(new DummySpanExporter());
}
@Bean
io.micrometer.tracing.Tracer customMicrometerTracer() {
return mock(io.micrometer.tracing.Tracer.class);
}
@Bean
EventPublisher customEventPublisher() {
return mock(EventPublisher.class);
}
@Bean
OtelCurrentTraceContext customOtelCurrentTraceContext() {
return mock(OtelCurrentTraceContext.class);
}
@Bean
SdkTracerProvider customSdkTracerProvider() {
return SdkTracerProvider.builder().build();
}
@Bean
ContextPropagators customContextPropagators() {
return mock(ContextPropagators.class);
}
@Bean
Sampler customSampler() {
return mock(Sampler.class);
}
@Bean
SpanProcessor customSpanProcessor() {
return mock(SpanProcessor.class);
}
@Bean
Tracer customTracer() {
return mock(Tracer.class);
}
@Bean
Slf4JEventListener customSlf4jEventListener() {
return new Slf4JEventListener();
}
@Bean
Slf4JBaggageEventListener customSlf4jBaggageEventListener() {
return new Slf4JBaggageEventListener(List.of("alpha"));
}
@Bean
OtelPropagator customOtelPropagator(ContextPropagators propagators, Tracer tracer) {
return new OtelPropagator(propagators, tracer);
}
@Bean
TextMapPropagator customTextMapPropagator() {
return mock(TextMapPropagator.class);
}
@Bean
SpanCustomizer customSpanCustomizer() {
return mock(SpanCustomizer.class);
}
}
@Configuration(proxyBeanMethods = false)
private static final class SdkTracerProviderCustomizationConfiguration {
@Bean
@Order(1)
SdkTracerProviderBuilderCustomizer sdkTracerProviderBuilderCustomizerOne() {
return (builder) -> {
SpanLimits spanLimits = SpanLimits.builder().setMaxNumberOfEvents(42).build();
builder.setSpanLimits(spanLimits);
};
}
@Bean
@Order(0)
SdkTracerProviderBuilderCustomizer sdkTracerProviderBuilderCustomizerTwo() {
return (builder) -> {
SpanLimits spanLimits = SpanLimits.builder().setMaxNumberOfEvents(21).build();
builder.setSpanLimits(spanLimits).setSampler(Sampler.alwaysOn());
};
}
}
private static final class DummySpanExporter implements SpanExporter {
@Override
public CompletableResultCode export(Collection<SpanData> spans) {
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode flush() {
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode shutdown() {
return CompletableResultCode.ofSuccess();
}
}
@Configuration(proxyBeanMethods = false)
private static final class InMemoryRecordingSpanExporterConfiguration {
@Bean
InMemoryRecordingSpanExporter spanExporter() {
return new InMemoryRecordingSpanExporter();
}
}
private static final class InMemoryRecordingSpanExporter implements SpanExporter {
private final List<SpanData> exportedSpans = new ArrayList<>();
private final CountDownLatch latch = new CountDownLatch(1);
@Override
public CompletableResultCode export(Collection<SpanData> spans) {
this.exportedSpans.addAll(spans);
this.latch.countDown();
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode flush() {
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode shutdown() {
this.exportedSpans.clear();
return CompletableResultCode.ofSuccess();
}
List<SpanData> getExportedSpans() {
return this.exportedSpans;
}
void await(Duration timeout) throws InterruptedException, TimeoutException {
if (!this.latch.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
throw new TimeoutException("Waiting for exporting spans timed out (%s)".formatted(timeout));
}
}
}
static class OtelEventListener implements EventListener {
private final List<Object> events = new ArrayList<>();
@Override
public void onEvent(Object event) {
this.events.add(event);
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SpanExporters}.
*
* @author Moritz Halbritter
*/
class SpanExportersTests {
@Test
void ofList() {
SpanExporter spanExporter1 = mock(SpanExporter.class);
SpanExporter spanExporter2 = mock(SpanExporter.class);
SpanExporters spanExporters = SpanExporters.of(List.of(spanExporter1, spanExporter2));
assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2);
assertThat(spanExporters.list()).containsExactly(spanExporter1, spanExporter2);
}
@Test
void ofArray() {
SpanExporter spanExporter1 = mock(SpanExporter.class);
SpanExporter spanExporter2 = mock(SpanExporter.class);
SpanExporters spanExporters = SpanExporters.of(spanExporter1, spanExporter2);
assertThat(spanExporters).containsExactly(spanExporter1, spanExporter2);
assertThat(spanExporters.list()).containsExactly(spanExporter1, spanExporter2);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import io.opentelemetry.sdk.trace.SpanProcessor;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link SpanProcessors}.
*
* @author Moritz Halbritter
*/
class SpanProcessorsTests {
@Test
void ofList() {
SpanProcessor spanProcessor1 = mock(SpanProcessor.class);
SpanProcessor spanProcessor2 = mock(SpanProcessor.class);
SpanProcessors spanProcessors = SpanProcessors.of(List.of(spanProcessor1, spanProcessor2));
assertThat(spanProcessors).containsExactly(spanProcessor1, spanProcessor2);
assertThat(spanProcessors.list()).containsExactly(spanProcessor1, spanProcessor2);
}
@Test
void ofArray() {
SpanProcessor spanProcessor1 = mock(SpanProcessor.class);
SpanProcessor spanProcessor2 = mock(SpanProcessor.class);
SpanProcessors spanProcessors = SpanProcessors.of(spanProcessor1, spanProcessor2);
assertThat(spanProcessors).containsExactly(spanProcessor1, spanProcessor2);
assertThat(spanProcessors.list()).containsExactly(spanProcessor1, spanProcessor2);
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.micrometer.core.instrument.observation.MeterObservationHandler;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler;
import io.micrometer.observation.ObservationRegistry.ObservationConfig;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler;
import io.micrometer.tracing.handler.TracingObservationHandler;
import org.assertj.core.extractor.Extractors;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.observation.autoconfigure.ObservationHandlerGroup;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
/**
* Tests for {@link TracingAndMeterObservationHandlerGroup}.
*
* @author Phillip Webb
*/
class TracingAndMeterObservationHandlerGroupTests {
@Test
void compareToSortsBeforeMeterObservationHandlerGroup() {
ObservationHandlerGroup meterGroup = ObservationHandlerGroup.of(MeterObservationHandler.class);
TracingAndMeterObservationHandlerGroup tracingAndMeterGroup = new TracingAndMeterObservationHandlerGroup(
mock(Tracer.class));
assertThat(sort(meterGroup, tracingAndMeterGroup)).containsExactly(tracingAndMeterGroup, meterGroup);
assertThat(sort(tracingAndMeterGroup, meterGroup)).containsExactly(tracingAndMeterGroup, meterGroup);
}
@Test
void isMemberAcceptsMeterObservationHandlerOrTracingObservationHandler() {
TracingAndMeterObservationHandlerGroup group = new TracingAndMeterObservationHandlerGroup(mock(Tracer.class));
assertThat(group.isMember(mock(ObservationHandler.class))).isFalse();
assertThat(group.isMember(mock(MeterObservationHandler.class))).isTrue();
assertThat(group.isMember(mock(TracingObservationHandler.class))).isTrue();
}
@Test
@SuppressWarnings("unchecked")
void registerMembersWrapsMeterObservationHandlersAndRegistersDistinctGroups() {
Tracer tracer = mock(Tracer.class);
TracingAndMeterObservationHandlerGroup group = new TracingAndMeterObservationHandlerGroup(tracer);
TracingObservationHandler<?> tracingHandler1 = mock(TracingObservationHandler.class);
TracingObservationHandler<?> tracingHandler2 = mock(TracingObservationHandler.class);
MeterObservationHandler<?> meterHandler1 = mock(MeterObservationHandler.class);
MeterObservationHandler<?> meterHandler2 = mock(MeterObservationHandler.class);
ObservationConfig config = mock(ObservationConfig.class);
List<ObservationHandler<?>> members = List.of(tracingHandler1, meterHandler1, tracingHandler2, meterHandler2);
group.registerMembers(config, members);
ArgumentCaptor<ObservationHandler<?>> handlerCaptor = ArgumentCaptor.captor();
then(config).should(times(2)).observationHandler(handlerCaptor.capture());
List<ObservationHandler<?>> actualComposites = handlerCaptor.getAllValues();
assertThat(actualComposites).hasSize(2);
ObservationHandler<?> tracingComposite = actualComposites.get(0);
assertThat(tracingComposite).isInstanceOf(FirstMatchingCompositeObservationHandler.class);
List<ObservationHandler<?>> tracingHandlers = (List<ObservationHandler<?>>) Extractors.byName("handlers")
.apply(tracingComposite);
assertThat(tracingHandlers).containsExactly(tracingHandler1, tracingHandler2);
ObservationHandler<?> metricsComposite = actualComposites.get(1);
assertThat(metricsComposite).isInstanceOf(FirstMatchingCompositeObservationHandler.class);
List<ObservationHandler<?>> metricsHandlers = (List<ObservationHandler<?>>) Extractors.byName("handlers")
.apply(metricsComposite);
assertThat(metricsHandlers).hasSize(2);
assertThat(metricsHandlers).extracting("delegate").containsExactly(meterHandler1, meterHandler2);
}
@Test
void registerMembersOnlyUsesCompositeWhenMoreThanOneHandler() {
Tracer tracer = mock(Tracer.class);
TracingAndMeterObservationHandlerGroup group = new TracingAndMeterObservationHandlerGroup(tracer);
TracingObservationHandler<?> tracingHandler1 = mock(TracingObservationHandler.class);
TracingObservationHandler<?> tracingHandler2 = mock(TracingObservationHandler.class);
MeterObservationHandler<?> meterHandler = mock(MeterObservationHandler.class);
ObservationConfig config = mock(ObservationConfig.class);
List<ObservationHandler<?>> members = List.of(tracingHandler1, meterHandler, tracingHandler2);
group.registerMembers(config, members);
ArgumentCaptor<ObservationHandler<?>> handlerCaptor = ArgumentCaptor.captor();
then(config).should(times(2)).observationHandler(handlerCaptor.capture());
List<ObservationHandler<?>> actualComposites = handlerCaptor.getAllValues();
assertThat(actualComposites).hasSize(2);
assertThat(actualComposites.get(0)).isInstanceOf(FirstMatchingCompositeObservationHandler.class);
assertThat(actualComposites.get(1)).isInstanceOf(TracingAwareMeterObservationHandler.class);
}
private List<ObservationHandlerGroup> sort(ObservationHandlerGroup... groups) {
List<ObservationHandlerGroup> list = new ArrayList<>(List.of(groups));
Collections.sort(list);
return list;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link TracingProperties}.
*
* @author Moritz Halbritter
*/
class TracingPropertiesTests {
@Test
void propagationTypeShouldOverrideProduceTypes() {
TracingProperties.Propagation propagation = new TracingProperties.Propagation();
propagation.setProduce(List.of(TracingProperties.Propagation.PropagationType.W3C));
propagation.setType(List.of(TracingProperties.Propagation.PropagationType.B3));
assertThat(propagation.getEffectiveProducedTypes())
.containsExactly(TracingProperties.Propagation.PropagationType.B3);
}
@Test
void propagationTypeShouldOverrideConsumeTypes() {
TracingProperties.Propagation propagation = new TracingProperties.Propagation();
propagation.setConsume(List.of(TracingProperties.Propagation.PropagationType.W3C));
propagation.setType(List.of(TracingProperties.Propagation.PropagationType.B3));
assertThat(propagation.getEffectiveConsumedTypes())
.containsExactly(TracingProperties.Propagation.PropagationType.B3);
}
@Test
void getEffectiveConsumeTypes() {
TracingProperties.Propagation propagation = new TracingProperties.Propagation();
propagation.setConsume(List.of(TracingProperties.Propagation.PropagationType.W3C));
assertThat(propagation.getEffectiveConsumedTypes())
.containsExactly(TracingProperties.Propagation.PropagationType.W3C);
}
@Test
void getEffectiveProduceTypes() {
TracingProperties.Propagation propagation = new TracingProperties.Propagation();
propagation.setProduce(List.of(TracingProperties.Propagation.PropagationType.W3C));
assertThat(propagation.getEffectiveProducedTypes())
.containsExactly(TracingProperties.Propagation.PropagationType.W3C);
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import io.micrometer.tracing.Tracer;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import okio.Buffer;
import okio.GzipSource;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.observation.autoconfigure.ObservationAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.tracing.autoconfigure.MicrometerTracingAutoConfiguration;
import org.springframework.boot.tracing.autoconfigure.otlp.OtlpTracingAutoConfigurationIntegrationTests.MockGrpcServer.RecordedGrpcRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link OtlpTracingAutoConfiguration}.
*
* @author Jonatan Ivanov
*/
class OtlpTracingAutoConfigurationIntegrationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.tracing.sampling.probability=1.0")
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
MicrometerTracingAutoConfiguration.class, OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.tracing.autoconfigure.OpenTelemetryTracingAutoConfiguration.class,
OtlpTracingAutoConfiguration.class));
private final MockWebServer mockWebServer = new MockWebServer();
private final MockGrpcServer mockGrpcServer = new MockGrpcServer();
@BeforeEach
void startServers() throws Exception {
this.mockWebServer.start();
this.mockGrpcServer.start();
}
@AfterEach
void stopServers() throws Exception {
this.mockWebServer.close();
this.mockGrpcServer.close();
}
@Test
void httpSpanExporterShouldUseProtobufAndNoCompressionByDefault() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:%d/v1/traces"
.formatted(this.mockWebServer.getPort()), "management.otlp.tracing.headers.custom=42")
.run((context) -> {
context.getBean(Tracer.class).nextSpan().name("test").end();
assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
.isSameAs(CompletableResultCode.ofSuccess());
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getRequestLine()).contains("/v1/traces");
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
assertThat(request.getHeader("custom")).isEqualTo("42");
assertThat(request.getBodySize()).isPositive();
try (Buffer body = request.getBody()) {
assertThat(body.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
}
});
}
@Test
void httpSpanExporterCanBeConfiguredToUseGzipCompression() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.otlp.tracing.compression=gzip",
"management.otlp.tracing.endpoint=http://localhost:%d/test".formatted(this.mockWebServer.getPort()))
.run((context) -> {
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class).hasSingleBean(SpanExporter.class);
context.getBean(Tracer.class).nextSpan().name("test").end();
assertThat(context.getBean(OtlpHttpSpanExporter.class).flush())
.isSameAs(CompletableResultCode.ofSuccess());
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.getRequestLine()).contains("/test");
assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf");
assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip");
assertThat(request.getBodySize()).isPositive();
try (Buffer uncompressed = new Buffer(); Buffer body = request.getBody()) {
uncompressed.writeAll(new GzipSource(body));
assertThat(uncompressed.readString(StandardCharsets.UTF_8)).contains("org.springframework.boot");
}
});
}
@Test
void grpcSpanExporterShouldExportSpans() {
this.contextRunner
.withPropertyValues(
"management.otlp.tracing.endpoint=http://localhost:%d".formatted(this.mockGrpcServer.getPort()),
"management.otlp.tracing.headers.custom=42", "management.otlp.tracing.transport=grpc")
.run((context) -> {
context.getBean(Tracer.class).nextSpan().name("test").end();
assertThat(context.getBean(OtlpGrpcSpanExporter.class).flush())
.isSameAs(CompletableResultCode.ofSuccess());
RecordedGrpcRequest request = this.mockGrpcServer.takeRequest(10, TimeUnit.SECONDS);
assertThat(request).isNotNull();
assertThat(request.headers().get("Content-Type")).isEqualTo("application/grpc");
assertThat(request.headers().get("custom")).isEqualTo("42");
assertThat(request.bodyAsString()).contains("org.springframework.boot");
});
}
static class MockGrpcServer {
private final Server server = createServer();
private final BlockingQueue<RecordedGrpcRequest> recordedRequests = new LinkedBlockingQueue<>();
void start() throws Exception {
this.server.start();
}
void close() throws Exception {
this.server.stop();
}
int getPort() {
return this.server.getURI().getPort();
}
RecordedGrpcRequest takeRequest(int timeout, TimeUnit unit) throws InterruptedException {
return this.recordedRequests.poll(timeout, unit);
}
void recordRequest(RecordedGrpcRequest request) {
this.recordedRequests.add(request);
}
private Server createServer() {
Server server = new Server();
server.addConnector(createConnector(server));
server.setHandler(new GrpcHandler());
return server;
}
private ServerConnector createConnector(Server server) {
ServerConnector connector = new ServerConnector(server,
new HTTP2CServerConnectionFactory(new HttpConfiguration()));
connector.setPort(0);
return connector;
}
class GrpcHandler extends Handler.Abstract {
@Override
public boolean handle(Request request, Response response, Callback callback) throws Exception {
try (InputStream in = Content.Source.asInputStream(request)) {
recordRequest(new RecordedGrpcRequest(request.getHeaders(), in.readAllBytes()));
}
response.getHeaders().add("Content-Type", "application/grpc");
response.getHeaders().add("Grpc-Status", "0");
callback.succeeded();
return true;
}
}
record RecordedGrpcRequest(HttpFields headers, byte[] body) {
String bodyAsString() {
return new String(this.body, StandardCharsets.UTF_8);
}
}
}
}

View File

@@ -0,0 +1,308 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.otlp;
import java.time.Duration;
import java.util.List;
import java.util.function.Supplier;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.exporter.internal.compression.GzipCompressor;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.trace.export.SpanExporter;
import okhttp3.HttpUrl;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.tracing.autoconfigure.otlp.OtlpTracingConfigurations.ConnectionDetails.PropertiesOtlpTracingConnectionDetails;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OtlpTracingAutoConfiguration}.
*
* @author Jonatan Ivanov
* @author Moritz Halbritter
* @author Eddú Meléndez
*/
class OtlpTracingAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OtlpTracingAutoConfiguration.class));
private final ApplicationContextRunner tracingDisabledContextRunner = this.contextRunner
.withPropertyValues("management.tracing.enabled=false");
@Test
void shouldNotSupplyBeansIfPropertyIsNotSet() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class));
}
@Test
void shouldNotSupplyBeansIfGrpcTransportIsEnabledButPropertyIsNotSet() {
this.contextRunner.withPropertyValues("management.otlp.tracing.transport=grpc")
.run((context) -> assertThat(context).doesNotHaveBean(OtlpGrpcSpanExporter.class));
}
@Test
void shouldSupplyBeans() {
this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces")
.run((context) -> assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class)
.hasSingleBean(SpanExporter.class));
}
@Test
void shouldCustomizeHttpTransportWithProperties() {
this.contextRunner
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4317/v1/traces",
"management.otlp.tracing.timeout=10m", "management.otlp.tracing.connect-timeout=20m",
"management.otlp.tracing.compression=GZIP", "management.otlp.tracing.headers.spring=boot")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class).hasSingleBean(SpanExporter.class);
OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class);
assertThat(exporter).extracting("delegate.httpSender.client")
.hasFieldOrPropertyWithValue("connectTimeoutMillis", 1200000)
.hasFieldOrPropertyWithValue("callTimeoutMillis", 600000);
assertThat(exporter).extracting("delegate.httpSender.compressor").isInstanceOf(GzipCompressor.class);
assertThat(exporter).extracting("delegate.httpSender.headerSupplier")
.asInstanceOf(InstanceOfAssertFactories.type(Supplier.class))
.satisfies((headerSupplier) -> assertThat(headerSupplier.get())
.asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class))
.containsEntry("spring", List.of("boot")));
});
}
@Test
void shouldSupplyBeansIfGrpcTransportIsEnabled() {
this.contextRunner
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4317/v1/traces",
"management.otlp.tracing.transport=grpc")
.run((context) -> assertThat(context).hasSingleBean(OtlpGrpcSpanExporter.class)
.hasSingleBean(SpanExporter.class));
}
@Test
void shouldCustomizeGrpcTransportWithProperties() {
this.contextRunner
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4317/v1/traces",
"management.otlp.tracing.transport=grpc", "management.otlp.tracing.timeout=10m",
"management.otlp.tracing.connect-timeout=20m", "management.otlp.tracing.compression=GZIP",
"management.otlp.tracing.headers.spring=boot")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpGrpcSpanExporter.class).hasSingleBean(SpanExporter.class);
OtlpGrpcSpanExporter exporter = context.getBean(OtlpGrpcSpanExporter.class);
assertThat(exporter).extracting("delegate.grpcSender.client")
.hasFieldOrPropertyWithValue("connectTimeoutMillis", 1200000)
.hasFieldOrPropertyWithValue("callTimeoutMillis", 600000);
assertThat(exporter).extracting("delegate.grpcSender.compressor").isInstanceOf(GzipCompressor.class);
assertThat(exporter).extracting("delegate.grpcSender.headersSupplier")
.asInstanceOf(InstanceOfAssertFactories.type(Supplier.class))
.satisfies((headerSupplier) -> assertThat(headerSupplier.get())
.asInstanceOf(InstanceOfAssertFactories.map(String.class, List.class))
.containsEntry("spring", List.of("boot")));
});
}
@Test
void shouldNotSupplyBeansIfGlobalTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.tracing.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
}
@Test
void shouldNotSupplyBeansIfOtlpTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.otlp.tracing.export.enabled=false")
.run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
}
@Test
void shouldNotSupplyBeansIfTracingBridgeIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
}
@Test
void shouldNotSupplyBeansIfOtelSdkIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.sdk"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
}
@Test
void shouldNotSupplyBeansIfOtelApiIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.api"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
}
@Test
void shouldNotSupplyBeansIfExporterIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.exporter"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanExporter.class));
}
@Test
void shouldBackOffWhenCustomHttpExporterIsDefined() {
this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class)
.run((context) -> assertThat(context).hasBean("customOtlpHttpSpanExporter")
.hasSingleBean(SpanExporter.class));
}
@Test
void shouldBackOffWhenCustomGrpcExporterIsDefined() {
this.contextRunner.withUserConfiguration(CustomGrpcExporterConfiguration.class)
.run((context) -> assertThat(context).hasBean("customOtlpGrpcSpanExporter")
.hasSingleBean(SpanExporter.class));
}
@Test
void shouldNotSupplyOtlpHttpSpanExporterIfTracingIsDisabled() {
this.tracingDisabledContextRunner
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces")
.run((context) -> assertThat(context).doesNotHaveBean(OtlpHttpSpanExporter.class));
}
@Test
void definesPropertiesBasedConnectionDetailsByDefault() {
this.contextRunner.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces")
.run((context) -> assertThat(context).hasSingleBean(PropertiesOtlpTracingConnectionDetails.class));
}
@Test
void testConnectionFactoryWithOverridesWhenUsingCustomConnectionDetails() {
this.contextRunner.withUserConfiguration(ConnectionDetailsConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(OtlpTracingConnectionDetails.class)
.doesNotHaveBean(PropertiesOtlpTracingConnectionDetails.class);
OtlpHttpSpanExporter otlpHttpSpanExporter = context.getBean(OtlpHttpSpanExporter.class);
assertThat(otlpHttpSpanExporter).extracting("delegate.httpSender.url")
.isEqualTo(HttpUrl.get("http://localhost:12345/v1/traces"));
});
}
@Test
void httpShouldUseMeterProviderIfSet() {
this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class)
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces")
.run((context) -> {
OtlpHttpSpanExporter otlpHttpSpanExporter = context.getBean(OtlpHttpSpanExporter.class);
assertThat(otlpHttpSpanExporter.toBuilder())
.extracting("delegate.meterProviderSupplier", InstanceOfAssertFactories.type(Supplier.class))
.satisfies((meterProviderSupplier) -> assertThat(meterProviderSupplier.get())
.isSameAs(MeterProviderConfiguration.meterProvider));
});
}
@Test
void grpcShouldUseMeterProviderIfSet() {
this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class)
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces",
"management.otlp.tracing.transport=grpc")
.run((context) -> {
OtlpGrpcSpanExporter otlpGrpcSpanExporter = context.getBean(OtlpGrpcSpanExporter.class);
assertThat(otlpGrpcSpanExporter.toBuilder())
.extracting("delegate.meterProviderSupplier", InstanceOfAssertFactories.type(Supplier.class))
.satisfies((meterProviderSupplier) -> assertThat(meterProviderSupplier.get())
.isSameAs(MeterProviderConfiguration.meterProvider));
});
}
@Test
void shouldCustomizeHttpTransportWithOtlpHttpSpanExporterBuilderCustomizer() {
Duration connectTimeout = Duration.ofMinutes(20);
Duration timeout = Duration.ofMinutes(10);
this.contextRunner
.withBean("httpCustomizer1", OtlpHttpSpanExporterBuilderCustomizer.class,
() -> (builder) -> builder.setConnectTimeout(connectTimeout))
.withBean("httpCustomizer2", OtlpHttpSpanExporterBuilderCustomizer.class,
() -> (builder) -> builder.setTimeout(timeout))
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4317/v1/traces")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpHttpSpanExporter.class).hasSingleBean(SpanExporter.class);
OtlpHttpSpanExporter exporter = context.getBean(OtlpHttpSpanExporter.class);
assertThat(exporter).extracting("delegate.httpSender.client")
.hasFieldOrPropertyWithValue("connectTimeoutMillis", (int) connectTimeout.toMillis())
.hasFieldOrPropertyWithValue("callTimeoutMillis", (int) timeout.toMillis());
});
}
@Test
void shouldCustomizeGrpcTransportWhenEnabledWithOtlpGrpcSpanExporterBuilderCustomizer() {
Duration timeout = Duration.ofMinutes(10);
Duration connectTimeout = Duration.ofMinutes(20);
this.contextRunner
.withBean("grpcCustomizer1", OtlpGrpcSpanExporterBuilderCustomizer.class,
() -> (builder) -> builder.setConnectTimeout(connectTimeout))
.withBean("grpcCustomizer2", OtlpGrpcSpanExporterBuilderCustomizer.class,
() -> (builder) -> builder.setTimeout(timeout))
.withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4317/v1/traces",
"management.otlp.tracing.transport=grpc")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpGrpcSpanExporter.class).hasSingleBean(SpanExporter.class);
OtlpGrpcSpanExporter exporter = context.getBean(OtlpGrpcSpanExporter.class);
assertThat(exporter).extracting("delegate.grpcSender.client")
.hasFieldOrPropertyWithValue("connectTimeoutMillis", (int) connectTimeout.toMillis())
.hasFieldOrPropertyWithValue("callTimeoutMillis", (int) timeout.toMillis());
});
}
@Configuration(proxyBeanMethods = false)
private static final class MeterProviderConfiguration {
static final MeterProvider meterProvider = (instrumentationScopeName) -> null;
@Bean
MeterProvider meterProvider() {
return meterProvider;
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomHttpExporterConfiguration {
@Bean
OtlpHttpSpanExporter customOtlpHttpSpanExporter() {
return OtlpHttpSpanExporter.builder().build();
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomGrpcExporterConfiguration {
@Bean
OtlpGrpcSpanExporter customOtlpGrpcSpanExporter() {
return OtlpGrpcSpanExporter.builder().build();
}
}
@Configuration(proxyBeanMethods = false)
static class ConnectionDetailsConfiguration {
@Bean
OtlpTracingConnectionDetails otlpTracingConnectionDetails() {
return (transport) -> "http://localhost:12345/v1/traces";
}
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.prometheus;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.TraceContext;
import io.micrometer.tracing.Tracer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.tracing.autoconfigure.prometheus.PrometheusExemplarsAutoConfiguration.LazyTracingSpanContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link LazyTracingSpanContext}.
*
* @author Andy Wilkinson
*/
class LazyTracingSpanContextTests {
private final Tracer tracer = mock(Tracer.class);
private final ObjectProvider<Tracer> objectProvider = new ObjectProvider<>() {
@Override
public Tracer getObject() throws BeansException {
return LazyTracingSpanContextTests.this.tracer;
}
@Override
public Tracer getObject(Object... args) throws BeansException {
return LazyTracingSpanContextTests.this.tracer;
}
@Override
public Tracer getIfAvailable() throws BeansException {
return LazyTracingSpanContextTests.this.tracer;
}
@Override
public Tracer getIfUnique() throws BeansException {
return LazyTracingSpanContextTests.this.tracer;
}
};
private final LazyTracingSpanContext spanContext = new LazyTracingSpanContext(this.objectProvider);
@Test
void whenCurrentSpanIsNullThenSpanIdIsNull() {
assertThat(this.spanContext.getCurrentSpanId()).isNull();
}
@Test
void whenCurrentSpanIsNullThenTraceIdIsNull() {
assertThat(this.spanContext.getCurrentTraceId()).isNull();
}
@Test
void whenCurrentSpanIsNullThenSampledIsFalse() {
assertThat(this.spanContext.isCurrentSpanSampled()).isFalse();
}
@Test
void whenCurrentSpanHasSpanIdThenSpanIdIsFromSpan() {
Span span = mock(Span.class);
given(this.tracer.currentSpan()).willReturn(span);
TraceContext traceContext = mock(TraceContext.class);
given(traceContext.spanId()).willReturn("span-id");
given(span.context()).willReturn(traceContext);
assertThat(this.spanContext.getCurrentSpanId()).isEqualTo("span-id");
}
@Test
void whenCurrentSpanHasTraceIdThenTraceIdIsFromSpan() {
Span span = mock(Span.class);
given(this.tracer.currentSpan()).willReturn(span);
TraceContext traceContext = mock(TraceContext.class);
given(traceContext.traceId()).willReturn("trace-id");
given(span.context()).willReturn(traceContext);
assertThat(this.spanContext.getCurrentTraceId()).isEqualTo("trace-id");
}
@Test
void whenCurrentSpanHasNoSpanIdThenSpanIdIsNull() {
Span span = mock(Span.class);
given(this.tracer.currentSpan()).willReturn(span);
TraceContext traceContext = mock(TraceContext.class);
given(span.context()).willReturn(traceContext);
assertThat(this.spanContext.getCurrentSpanId()).isNull();
}
@Test
void whenCurrentSpanHasNoTraceIdThenTraceIdIsNull() {
Span span = mock(Span.class);
given(this.tracer.currentSpan()).willReturn(span);
TraceContext traceContext = mock(TraceContext.class);
given(span.context()).willReturn(traceContext);
assertThat(this.spanContext.getCurrentTraceId()).isNull();
}
@Test
void whenCurrentSpanIsSampledThenSampledIsTrue() {
Span span = mock(Span.class);
given(this.tracer.currentSpan()).willReturn(span);
TraceContext traceContext = mock(TraceContext.class);
given(traceContext.sampled()).willReturn(true);
given(span.context()).willReturn(traceContext);
assertThat(this.spanContext.isCurrentSpanSampled()).isTrue();
}
@Test
void whenCurrentSpanIsNotSampledThenSampledIsFalse() {
Span span = mock(Span.class);
given(this.tracer.currentSpan()).willReturn(span);
TraceContext traceContext = mock(TraceContext.class);
given(traceContext.sampled()).willReturn(false);
given(span.context()).willReturn(traceContext);
assertThat(this.spanContext.isCurrentSpanSampled()).isFalse();
}
@Test
void whenCurrentSpanHasDeferredSamplingThenSampledIsFalse() {
Span span = mock(Span.class);
given(this.tracer.currentSpan()).willReturn(span);
TraceContext traceContext = mock(TraceContext.class);
given(traceContext.sampled()).willReturn(null);
given(span.context()).willReturn(traceContext);
assertThat(this.spanContext.isCurrentSpanSampled()).isFalse();
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.prometheus;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import io.micrometer.tracing.Tracer;
import io.micrometer.tracing.handler.TracingAwareMeterObservationHandler;
import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter;
import io.prometheus.metrics.tracer.common.SpanContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.metrics.autoconfigure.MetricsAutoConfiguration;
import org.springframework.boot.metrics.autoconfigure.export.prometheus.PrometheusMetricsExportAutoConfiguration;
import org.springframework.boot.observation.autoconfigure.ObservationAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.tracing.autoconfigure.BraveAutoConfiguration;
import org.springframework.boot.tracing.autoconfigure.MicrometerTracingAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PrometheusExemplarsAutoConfiguration}.
*
* @author Jonatan Ivanov
*/
class PrometheusExemplarsAutoConfigurationTests {
private static final Pattern BUCKET_TRACE_INFO_PATTERN = Pattern.compile(
"^test_observation_seconds_bucket\\{error=\"none\",le=\".+\"} 1 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$");
private static final Pattern COUNT_TRACE_INFO_PATTERN = Pattern.compile(
"^test_observation_seconds_count\\{error=\"none\"} 1 # \\{span_id=\"(\\p{XDigit}+)\",trace_id=\"(\\p{XDigit}+)\"} .+$");
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.tracing.sampling.probability=1.0",
"management.metrics.distribution.percentiles-histogram.all=true",
"management.metrics.use-global-registry=false")
.withConfiguration(
AutoConfigurations.of(MetricsAutoConfiguration.class, PrometheusMetricsExportAutoConfiguration.class,
PrometheusExemplarsAutoConfiguration.class, ObservationAutoConfiguration.class,
BraveAutoConfiguration.class, MicrometerTracingAutoConfiguration.class));
@Test
void shouldNotSupplyBeansIfPrometheusSupportIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.prometheus.metrics.tracer"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class));
}
@Test
void shouldNotSupplyBeansIfMicrometerTracingIsMissing() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.micrometer.tracing"))
.run((context) -> assertThat(context).doesNotHaveBean(SpanContext.class));
}
@Test
void shouldSupplyCustomBeans() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(SpanContext.class)
.getBean(SpanContext.class)
.isSameAs(CustomConfiguration.SPAN_CONTEXT));
}
@Test
void prometheusOpenMetricsOutputWithoutExemplarsOnHistogramCount() {
this.contextRunner.withUserConfiguration(TracingConfiguration.class)
.withPropertyValues(
"management.prometheus.metrics.export.properties.io.prometheus.exporter.exemplarsOnAllMetricTypes=false")
.run((context) -> {
assertThat(context).hasSingleBean(SpanContext.class);
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
Observation.start("test.observation", observationRegistry).stop();
PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class);
String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE);
assertThat(openMetricsOutput).contains("test_observation_seconds_bucket");
assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count");
assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(1);
assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(1);
Optional<TraceInfo> bucketTraceInfo = openMetricsOutput.lines()
.filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id"))
.map(BUCKET_TRACE_INFO_PATTERN::matcher)
.flatMap(Matcher::results)
.map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1)))
.findFirst();
assertThat(bucketTraceInfo).isNotEmpty();
});
}
@Test
void prometheusOpenMetricsOutputShouldContainExemplars() {
this.contextRunner.withUserConfiguration(TracingConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(SpanContext.class);
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
Observation.start("test.observation", observationRegistry).stop();
PrometheusMeterRegistry prometheusMeterRegistry = context.getBean(PrometheusMeterRegistry.class);
String openMetricsOutput = prometheusMeterRegistry.scrape(OpenMetricsTextFormatWriter.CONTENT_TYPE);
assertThat(openMetricsOutput).contains("test_observation_seconds_bucket");
assertThat(openMetricsOutput).containsOnlyOnce("test_observation_seconds_count");
assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "span_id")).isEqualTo(2);
assertThat(StringUtils.countOccurrencesOf(openMetricsOutput, "trace_id")).isEqualTo(2);
Optional<TraceInfo> bucketTraceInfo = openMetricsOutput.lines()
.filter((line) -> line.contains("test_observation_seconds_bucket") && line.contains("span_id"))
.map(BUCKET_TRACE_INFO_PATTERN::matcher)
.flatMap(Matcher::results)
.map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1)))
.findFirst();
Optional<TraceInfo> counterTraceInfo = openMetricsOutput.lines()
.filter((line) -> line.contains("test_observation_seconds_count") && line.contains("span_id"))
.map(COUNT_TRACE_INFO_PATTERN::matcher)
.flatMap(Matcher::results)
.map((matchResult) -> new TraceInfo(matchResult.group(2), matchResult.group(1)))
.findFirst();
assertThat(bucketTraceInfo).isNotEmpty().contains(counterTraceInfo.orElse(null));
});
}
@Configuration(proxyBeanMethods = false)
private static final class CustomConfiguration {
static final SpanContext SPAN_CONTEXT = mock(SpanContext.class);
@Bean
SpanContext customSpanContext() {
return SPAN_CONTEXT;
}
}
private record TraceInfo(String traceId, String spanId) {
}
@Configuration(proxyBeanMethods = false)
static class TracingConfiguration {
@Bean
TracingAwareMeterObservationHandler<Observation.Context> tracingAwareMeterObservationHandler(
MeterRegistry meterRegistry, Tracer tracer) {
DefaultMeterObservationHandler delegate = new DefaultMeterObservationHandler(meterRegistry);
return new TracingAwareMeterObservationHandler<>(delegate, tracer);
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.zipkin;
import zipkin2.reporter.Encoding;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.zipkin.autoconfigure.ZipkinAutoConfiguration;
import org.springframework.context.annotation.Bean;
/**
* Configures the bean {@linkplain ZipkinAutoConfiguration} would from properties.
*/
@TestConfiguration(proxyBeanMethods = false)
class DefaultEncodingConfiguration {
@Bean
@ConditionalOnMissingBean
Encoding zipkinReporterEncoding() {
return Encoding.JSON;
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.zipkin;
import java.io.IOException;
import java.util.List;
import zipkin2.reporter.BytesMessageSender;
import zipkin2.reporter.Encoding;
class NoopSender extends BytesMessageSender.Base {
NoopSender(Encoding encoding) {
super(encoding);
}
@Override
public int messageMaxBytes() {
return 1024;
}
@Override
public void send(List<byte[]> encodedSpans) {
}
@Override
public void close() throws IOException {
}
}

View File

@@ -0,0 +1,241 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.zipkin;
import java.nio.charset.StandardCharsets;
import brave.Tag;
import brave.handler.MutableSpan;
import brave.handler.SpanHandler;
import brave.propagation.TraceContext;
import org.junit.jupiter.api.Test;
import zipkin2.reporter.BytesEncoder;
import zipkin2.reporter.BytesMessageSender;
import zipkin2.reporter.Encoding;
import zipkin2.reporter.brave.AsyncZipkinSpanHandler;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.tracing.autoconfigure.zipkin.ZipkinTracingAutoConfiguration.BraveConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link BraveConfiguration}.
*
* @author Moritz Halbritter
*/
class ZipkinConfigurationsBraveConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DefaultEncodingConfiguration.class, BraveConfiguration.class));
@Test
void shouldSupplyBeans() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class));
}
@Test
void shouldNotSupplySpanHandlerIfReporterIsMissing() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(AsyncZipkinSpanHandler.class));
}
@Test
void shouldNotSupplyIfZipkinReporterBraveIsNotOnClasspath() {
// Note: Technically, Brave can work without zipkin-reporter. We also need this
// for any configuration that uses senders defined in the Spring Boot source tree,
// such as HttpSender.
this.contextRunner.withClassLoader(new FilteredClassLoader("zipkin2.reporter.brave"))
.withUserConfiguration(SenderConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(AsyncZipkinSpanHandler.class));
}
@Test
void shouldBackOffOnCustomBeans() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomConfiguration.class)
.run((context) -> {
assertThat(context).hasBean("customAsyncZipkinSpanHandler");
assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class);
});
}
@Test
void shouldSupplyAsyncZipkinSpanHandlerWithCustomSpanHandler() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomSpanHandlerConfiguration.class)
.run((context) -> {
assertThat(context).hasBean("customSpanHandler");
assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class);
});
}
@Test
void shouldNotSupplyAsyncZipkinSpanHandlerIfGlobalTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.tracing.enabled=false")
.withUserConfiguration(SenderConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(AsyncZipkinSpanHandler.class));
}
@Test
void shouldNotSupplyAsyncZipkinSpanHandlerIfZipkinTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.zipkin.tracing.export.enabled=false")
.withUserConfiguration(SenderConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(AsyncZipkinSpanHandler.class));
}
@Test
void shouldUseCustomEncoderBean() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomEncoderConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class);
assertThat(context.getBean(AsyncZipkinSpanHandler.class)).extracting("spanReporter.encoder")
.isInstanceOf(CustomMutableSpanEncoder.class)
.extracting("encoding")
.isEqualTo(Encoding.JSON);
});
}
@Test
void shouldUseCustomEncodingBean() {
this.contextRunner
.withUserConfiguration(SenderConfiguration.class, CustomEncodingConfiguration.class,
CustomEncoderConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(AsyncZipkinSpanHandler.class);
assertThat(context.getBean(AsyncZipkinSpanHandler.class)).extracting("encoding")
.isEqualTo(Encoding.PROTO3);
});
}
@Test
void shouldUseDefaultThrowableTagBean() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class).run((context) -> {
@SuppressWarnings("unchecked")
BytesEncoder<MutableSpan> encoder = context.getBean(BytesEncoder.class);
MutableSpan span = createTestSpan();
// default tag key name is "error", and doesn't overwrite
assertThat(new String(encoder.encode(span), StandardCharsets.UTF_8)).isEqualTo(
"{\"traceId\":\"0000000000000001\",\"id\":\"0000000000000001\",\"tags\":{\"error\":\"true\"}}");
});
}
@Test
void shouldUseCustomThrowableTagBean() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomThrowableTagConfiguration.class)
.run((context) -> {
@SuppressWarnings("unchecked")
BytesEncoder<MutableSpan> encoder = context.getBean(BytesEncoder.class);
MutableSpan span = createTestSpan();
// The custom throwable parser doesn't use the key "error" we can see both
assertThat(new String(encoder.encode(span), StandardCharsets.UTF_8)).isEqualTo(
"{\"traceId\":\"0000000000000001\",\"id\":\"0000000000000001\",\"tags\":{\"error\":\"true\",\"exception\":\"ice cream\"}}");
});
}
private MutableSpan createTestSpan() {
MutableSpan span = new MutableSpan();
span.traceId("1");
span.id("1");
span.tag("error", "true");
span.error(new RuntimeException("ice cream"));
return span;
}
@Configuration(proxyBeanMethods = false)
private static final class SenderConfiguration {
@Bean
BytesMessageSender sender(Encoding encoding) {
return new NoopSender(encoding);
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomConfiguration {
@Bean
AsyncZipkinSpanHandler customAsyncZipkinSpanHandler() {
return AsyncZipkinSpanHandler.create(new NoopSender(Encoding.JSON));
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomThrowableTagConfiguration {
@Bean
Tag<Throwable> throwableTag() {
return new Tag<>("exception") {
@Override
protected String parseValue(Throwable throwable, TraceContext traceContext) {
return throwable.getMessage();
}
};
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomSpanHandlerConfiguration {
@Bean
SpanHandler customSpanHandler() {
return mock(SpanHandler.class);
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomEncodingConfiguration {
@Bean
Encoding encoding() {
return Encoding.PROTO3;
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomEncoderConfiguration {
@Bean
BytesEncoder<MutableSpan> encoder(Encoding encoding) {
return new CustomMutableSpanEncoder(encoding);
}
}
private record CustomMutableSpanEncoder(Encoding encoding) implements BytesEncoder<MutableSpan> {
@Override
public int sizeInBytes(MutableSpan span) {
throw new UnsupportedOperationException();
}
@Override
public byte[] encode(MutableSpan span) {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* 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
*
* https://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.springframework.boot.tracing.autoconfigure.zipkin;
import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter;
import org.junit.jupiter.api.Test;
import zipkin2.Span;
import zipkin2.reporter.BytesEncoder;
import zipkin2.reporter.BytesMessageSender;
import zipkin2.reporter.Encoding;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.tracing.autoconfigure.zipkin.ZipkinTracingAutoConfiguration.OpenTelemetryConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OpenTelemetryConfiguration}.
*
* @author Moritz Halbritter
*/
class ZipkinConfigurationsOpenTelemetryConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DefaultEncodingConfiguration.class, OpenTelemetryConfiguration.class));
@Test
void shouldSupplyBeans() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomEncoderConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(ZipkinSpanExporter.class);
assertThat(context).hasBean("customSpanEncoder");
});
}
@Test
void shouldNotSupplyZipkinSpanExporterIfSenderIsMissing() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class);
assertThat(context).hasBean("spanBytesEncoder");
});
}
@Test
void shouldNotSupplyZipkinSpanExporterIfNotOnClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.exporter.zipkin"))
.withUserConfiguration(SenderConfiguration.class)
.run((context) -> {
assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class);
assertThat(context).doesNotHaveBean("spanBytesEncoder");
});
}
@Test
void shouldBackOffIfZipkinIsNotOnClasspath() {
this.contextRunner.withClassLoader(new FilteredClassLoader("zipkin2.Span"))
.withUserConfiguration(SenderConfiguration.class)
.run((context) -> {
assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class);
assertThat(context).doesNotHaveBean("spanBytesEncoder");
});
}
@Test
void shouldBackOffOnCustomBeans() {
this.contextRunner.withUserConfiguration(CustomConfiguration.class).run((context) -> {
assertThat(context).hasBean("customZipkinSpanExporter");
assertThat(context).hasSingleBean(ZipkinSpanExporter.class);
});
}
@Test
void shouldNotSupplyZipkinSpanExporterIfGlobalTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.tracing.enabled=false")
.withUserConfiguration(SenderConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class));
}
@Test
void shouldNotSupplyZipkinSpanExporterIfZipkinTracingIsDisabled() {
this.contextRunner.withPropertyValues("management.zipkin.tracing.export.enabled=false")
.withUserConfiguration(SenderConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(ZipkinSpanExporter.class));
}
@Test
void shouldUseCustomEncoderBean() {
this.contextRunner.withUserConfiguration(SenderConfiguration.class, CustomEncoderConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(ZipkinSpanExporter.class);
assertThat(context).hasBean("customSpanEncoder");
assertThat(context.getBean(ZipkinSpanExporter.class)).extracting("encoder")
.isInstanceOf(CustomSpanEncoder.class)
.extracting("encoding")
.isEqualTo(Encoding.JSON);
});
}
@Test
void shouldUseCustomEncodingBean() {
this.contextRunner
.withUserConfiguration(SenderConfiguration.class, CustomEncodingConfiguration.class,
CustomEncoderConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(ZipkinSpanExporter.class);
assertThat(context).hasBean("customSpanEncoder");
assertThat(context.getBean(ZipkinSpanExporter.class)).extracting("encoder")
.isInstanceOf(CustomSpanEncoder.class)
.extracting("encoding")
.isEqualTo(Encoding.PROTO3);
});
}
@Configuration(proxyBeanMethods = false)
private static final class CustomEncodingConfiguration {
@Bean
Encoding encoding() {
return Encoding.PROTO3;
}
}
@Configuration(proxyBeanMethods = false)
private static final class SenderConfiguration {
@Bean
BytesMessageSender sender(Encoding encoding) {
return new NoopSender(encoding);
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomConfiguration {
@Bean
ZipkinSpanExporter customZipkinSpanExporter() {
return ZipkinSpanExporter.builder().build();
}
}
@Configuration(proxyBeanMethods = false)
private static final class CustomEncoderConfiguration {
@Bean
BytesEncoder<Span> customSpanEncoder(Encoding encoding) {
return new CustomSpanEncoder(encoding);
}
}
record CustomSpanEncoder(Encoding encoding) implements BytesEncoder<Span> {
@Override
public int sizeInBytes(Span span) {
throw new UnsupportedOperationException();
}
@Override
public byte[] encode(Span span) {
throw new UnsupportedOperationException();
}
}
}