Create spring-boot-tracing module
This commit is contained in:
committed by
Phillip Webb
parent
f680582019
commit
fb886a1818
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 "";
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.tracing.autoconfigure.OpenTelemetryEventPublisherBeansTestExecutionListener
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user