Add some infrastructure for Observation (#3879)

* Add some infrastructure for Observation

* Populate an `ObservationRegistry` bean from the `IntegrationManagementConfigurer`
into all the `IntegrationManagement` components
* Introduce `MessageReceiverContext` and `MessageSenderContext` for easier usage
in the target code
* Implement `Observation` handling in the `AbstractMessageHandler`
* Modify `ObservationPropagationChannelInterceptorTests` for new `MessageSenderContext`
* Use `BridgeHandler` to ensure that `Observation` is propagated and handled properly
* Verify that tags from the `AbstractMessageHandler` are preset on the consumer span

* * Add a `DocumentedObservation` infrastructure

* * Add `Timer` verification to the propagation test

* * Update to the latest Observation API

* * Add custom observation convention support for the `AbstractMessageHandler`
* Use more meaningful prefix for Spring Integration tags

* * Move singleton instance for `DefaultMessageReceiverObservationConvention`
into `DefaultMessageReceiverObservationConvention` per se as an `INSTANCE` constant
* Use `MeterRegistryAssert` in the `ObservationPropagationChannelInterceptorTests`
to verify meters emitted

* * And an integration test with Zipkin based on the `SampleTestRunner`
This commit is contained in:
Artem Bilan
2022-09-19 10:22:04 -04:00
committed by GitHub
parent 5ece0e0dfd
commit 8c73a2d0cd
13 changed files with 595 additions and 73 deletions

View File

@@ -21,12 +21,12 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -40,22 +40,30 @@ import org.springframework.integration.channel.ExecutorChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.support.MutableMessage;
import org.springframework.integration.support.MutableMessageBuilder;
import org.springframework.integration.support.management.observation.IntegrationObservation;
import org.springframework.integration.support.management.observation.MessageSenderContext;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import io.micrometer.common.KeyValues;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.micrometer.core.tck.MeterRegistryAssert;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import io.micrometer.observation.transport.ReceiverContext;
import io.micrometer.observation.transport.SenderContext;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.TraceContext;
import io.micrometer.tracing.Tracer;
@@ -79,6 +87,9 @@ public class ObservationPropagationChannelInterceptorTests {
@Autowired
ObservationRegistry observationRegistry;
@Autowired
MeterRegistry meterRegistry;
@Autowired
SimpleTracer simpleTracer;
@@ -199,36 +210,29 @@ public class ObservationPropagationChannelInterceptorTests {
}
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
void observationContextPropagatedOverDirectChannel() throws InterruptedException {
CountDownLatch handleLatch = new CountDownLatch(1);
this.testTracingChannel.subscribe(m -> {
// This would be the instrumentation code on the receiver side
// We would need to check if Zipkin wouldn't require us to create the receiving span and then an additional one for the user code...
ReceiverContext<Message<?>> receiverContext =
new ReceiverContext<>((carrier, key) -> carrier.getHeaders().get(key, String.class));
receiverContext.setCarrier(m);
void observationContextPropagatedOverExecutorChannel() {
BridgeHandler handler = new BridgeHandler();
handler.registerObservationRegistry(this.observationRegistry);
handler.setBeanName("testBridge");
this.testTracingChannel.subscribe(handler);
// ...and this would be the user's code
Observation.createNotStarted("user.code", receiverContext, this.observationRegistry)
.observe(() -> {
// Let's assume that this is the user code
});
handleLatch.countDown();
});
QueueChannel replyChannel = new QueueChannel();
// This would be the instrumentation code on the sender side (user's code would call e.g. MessageTemplate and this code
// would lay in MessageTemplate)
// We need to mutate the carrier, so we need to use the builder not the message since message headers are immutable
SenderContext<MessageBuilder<String>> senderContext =
new SenderContext<>((carrier, key, value) -> Objects.requireNonNull(carrier).setHeader(key, value));
MessageBuilder<String> builder = MessageBuilder.withPayload("test");
senderContext.setCarrier(builder);
MutableMessage<String> message =
(MutableMessage<String>) MutableMessageBuilder.withPayload("test")
.setHeader(MessageHeaders.REPLY_CHANNEL, replyChannel)
.build();
Observation.createNotStarted("sending", senderContext, this.observationRegistry)
.observe(() -> this.testTracingChannel.send(builder.build()));
Observation.createNotStarted("sending", new MessageSenderContext(message), this.observationRegistry)
.observe(() -> this.testTracingChannel.send(message));
assertThat(handleLatch.await(10, TimeUnit.SECONDS)).isTrue();
Message<?> receive = replyChannel.receive();
assertThat(receive).isNotNull()
.extracting(Message::getHeaders)
.asInstanceOf(InstanceOfAssertFactories.MAP)
.containsEntry("foo", "some foo value")
.containsEntry("bar", "some bar value");
TestObservationRegistryAssert.assertThat(this.observationRegistry)
.doesNotHaveAnyRemainingCurrentObservation();
@@ -236,12 +240,28 @@ public class ObservationPropagationChannelInterceptorTests {
TracerAssert.assertThat(this.simpleTracer)
.reportedSpans()
.hasSize(2)
.satisfies(simpleSpans -> SpansAssert.assertThat((Collection<FinishedSpan>) (Collection) simpleSpans)
.satisfies(simpleSpans -> assertSpans(simpleSpans)
.hasASpanWithName("sending")
.assertThatASpanWithNameEqualTo("user.code")
.assertThatASpanWithNameEqualTo("testBridge receive")
.hasTag("foo", "some foo value")
.hasTag("bar", "some bar value")
.hasTag("spring.integration.type", "handler")
.hasTag("spring.integration.name", "testBridge")
.hasKindEqualTo(Span.Kind.CONSUMER));
MeterRegistryAssert.assertThat(this.meterRegistry)
.hasTimerWithNameAndTags("spring.integration.handler",
KeyValues.of(IntegrationObservation.HandlerTags.COMPONENT_NAME.asString(), "testBridge",
IntegrationObservation.HandlerTags.COMPONENT_TYPE.asString(), "handler",
"error", "none"));
assertThat(this.meterRegistry.get("spring.integration.handler").timer().count()).isEqualTo(1);
}
@SuppressWarnings("unchecked")
private static SpansAssert assertSpans(Collection<? extends FinishedSpan> actual) {
return SpansAssert.assertThat((Collection<FinishedSpan>) actual);
}
@Configuration
@@ -254,17 +274,24 @@ public class ObservationPropagationChannelInterceptorTests {
}
@Bean
ObservationRegistry observationRegistry(Tracer tracer, Propagator propagator) {
MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
@Bean
ObservationRegistry observationRegistry(Tracer tracer, Propagator propagator, MeterRegistry meterRegistry) {
TestObservationRegistry observationRegistry = TestObservationRegistry.create();
observationRegistry.observationConfig().observationHandler(
// Composite will pick the first matching handler
new ObservationHandler.FirstMatchingCompositeObservationHandler(
// This is responsible for creating a child span on the sender side
new PropagatingSenderTracingObservationHandler<>(tracer, propagator),
// This is responsible for creating a span on the receiver side
new PropagatingReceiverTracingObservationHandler<>(tracer, propagator),
// This is responsible for creating a default span
new DefaultTracingObservationHandler(tracer)));
observationRegistry.observationConfig()
.observationHandler(new DefaultMeterObservationHandler(meterRegistry))
.observationHandler(
// Composite will pick the first matching handler
new ObservationHandler.FirstMatchingCompositeObservationHandler(
// This is responsible for creating a child span on the sender side
new PropagatingSenderTracingObservationHandler<>(tracer, propagator),
// This is responsible for creating a span on the receiver side
new PropagatingReceiverTracingObservationHandler<>(tracer, propagator),
// This is responsible for creating a default span
new DefaultTracingObservationHandler(tracer)));
return observationRegistry;
}
@@ -318,14 +345,13 @@ public class ObservationPropagationChannelInterceptorTests {
setter.set(carrier, "bar", "some bar value");
}
// This is called on the consumer side when the message is consumed
// Normally we would use tools like Extractor from tracing but for tests we are just manually creating a span
@Override
public <C> Span.Builder extract(C carrier, Getter<C> getter) {
String foo = getter.get(carrier, "foo");
String bar = getter.get(carrier, "bar");
return tracer.spanBuilder().kind(Span.Kind.CONSUMER).tag("foo", foo).tag("bar", bar);
return tracer.spanBuilder().tag("foo", foo).tag("bar", bar);
}
};
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright 2022 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.integration.support.management.observation;
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.BridgeTo;
import org.springframework.integration.annotation.EndpointId;
import org.springframework.integration.annotation.Poller;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.channel.interceptor.ObservationPropagationChannelInterceptor;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.config.EnableIntegrationManagement;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.integration.support.MutableMessage;
import org.springframework.integration.support.MutableMessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import io.micrometer.common.KeyValues;
import io.micrometer.core.tck.MeterRegistryAssert;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.test.SampleTestRunner;
import io.micrometer.tracing.test.simple.SpansAssert;
/**
* @author Artem Bilan
*
* @since 6.0
*/
public class IntegrationObservabilityZipkinTests extends SampleTestRunner {
@Override
public TracingSetup[] getTracingSetup() {
return new TracingSetup[]{ TracingSetup.IN_MEMORY_BRAVE, TracingSetup.ZIPKIN_BRAVE };
}
@Override
public SampleTestRunnerConsumer yourCode() {
return (bb, meterRegistry) -> {
ObservationRegistry observationRegistry = getObservationRegistry();
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) {
applicationContext.registerBean(ObservationRegistry.class, () -> observationRegistry);
applicationContext.register(ObservationIntegrationTestConfiguration.class);
applicationContext.refresh();
PollableChannel queueChannel = applicationContext.getBean("queueChannel", PollableChannel.class);
PollableChannel replyChannel = new QueueChannel();
MutableMessage<String> testMessage =
(MutableMessage<String>) MutableMessageBuilder.withPayload("test data")
.setHeader(MessageHeaders.REPLY_CHANNEL, replyChannel)
.build();
Observation.createNotStarted("Test send", new MessageSenderContext(testMessage), observationRegistry)
.observe(() -> queueChannel.send(testMessage));
Message<?> receive = replyChannel.receive(10_000);
assertThat(receive).isNotNull()
.extracting("payload").isEqualTo("test data");
}
SpansAssert.assertThat(bb.getFinishedSpans())
.haveSameTraceId()
.hasASpanWithName("Test send", spanAssert -> spanAssert.hasKindEqualTo(Span.Kind.PRODUCER))
.hasASpanWithName("observedEndpoint receive", spanAssert -> spanAssert
.hasTag(IntegrationObservation.HandlerTags.COMPONENT_NAME.asString(), "observedEndpoint")
.hasTag(IntegrationObservation.HandlerTags.COMPONENT_TYPE.asString(), "handler")
.hasKindEqualTo(Span.Kind.CONSUMER))
.hasSize(2);
MeterRegistryAssert.assertThat(getMeterRegistry())
.hasTimerWithNameAndTags("spring.integration.handler",
KeyValues.of(
IntegrationObservation.HandlerTags.COMPONENT_NAME.asString(), "observedEndpoint",
IntegrationObservation.HandlerTags.COMPONENT_TYPE.asString(), "handler",
"error", "none"));
};
}
@Configuration
@EnableIntegration
@EnableIntegrationManagement
public static class ObservationIntegrationTestConfiguration {
@Bean
@GlobalChannelInterceptor
public ChannelInterceptor observationPropagationInterceptor(ObservationRegistry observationRegistry) {
return new ObservationPropagationChannelInterceptor(observationRegistry);
}
@Bean
@BridgeTo(poller = @Poller(fixedDelay = "100"))
@EndpointId("observedEndpoint")
public PollableChannel queueChannel() {
return new QueueChannel();
}
}
}