diff --git a/docs/modules/ROOT/pages/spring-cloud-bus/configuration.adoc b/docs/modules/ROOT/pages/spring-cloud-bus/configuration.adoc index fe9d44bf..c861eacb 100644 --- a/docs/modules/ROOT/pages/spring-cloud-bus/configuration.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-bus/configuration.adoc @@ -24,42 +24,37 @@ NOTE: If you have enabled https://docs.spring.io/spring-cloud-config/reference/c [[tracing-bus-events]] == Tracing Bus Events -Bus events (subclasses of `RemoteApplicationEvent`) can be traced by setting -`spring.cloud.bus.trace.enabled=true`. If you do so, the Spring Boot `TraceRepository` -(if it is present) shows each event sent and all the acks from each service instance. The -following example comes from the `/trace` endpoint: +Bus events (subclasses of `RemoteApplicationEvent`) can be traced by setting `spring.cloud.bus.trace.enabled=true` and `management.endpoints.web.exposure.include=bustrace`. +When enabled, the Spring Cloud Bus `TraceRepository` (by default, an `InMemoryTraceRepository`, or you can provide your own `TraceRepository` bean) records each event sent and all acks from each service instance. +The following example comes from the `/actuator/bustrace` endpoint: [source,json] ---- { - "timestamp": "2015-11-26T10:24:44.411+0000", - "info": { - "signal": "spring.cloud.bus.ack", - "type": "RefreshRemoteApplicationEvent", - "id": "c4d374b7-58ea-4928-a312-31984def293b", - "origin": "stores:8081", - "destination": "*:**" - } - }, - { - "timestamp": "2015-11-26T10:24:41.864+0000", - "info": { - "signal": "spring.cloud.bus.sent", - "type": "RefreshRemoteApplicationEvent", - "id": "c4d374b7-58ea-4928-a312-31984def293b", - "origin": "customers:9000", - "destination": "*:**" - } - }, - { - "timestamp": "2015-11-26T10:24:41.862+0000", - "info": { - "signal": "spring.cloud.bus.ack", - "type": "RefreshRemoteApplicationEvent", - "id": "c4d374b7-58ea-4928-a312-31984def293b", - "origin": "customers:9000", - "destination": "*:**" - } + "traces": [ + { + "timestamp": "2026-08-23T06:38:29.731486Z", + "origin": "stores:8081", + "signal": "spring.cloud.bus.ack", + "type": "RefreshRemoteApplicationEvent" + }, + { + "timestamp": "2026-08-23T06:38:25.059455800Z", + "destination": "**:**", + "id": "2141f38c-7987-46a0-997a-ac95849365e0", + "origin": "customers:9000", + "signal": "spring.cloud.bus.sent", + "type": "RefreshRemoteApplicationEvent" + }, + { + "timestamp": "2026-08-23T06:38:25.058947100Z", + "destination": "**", + "id": "2141f38c-7987-46a0-997a-ac95849365e0", + "origin": "customers:9000", + "signal": "spring.cloud.bus.ack", + "type": "RefreshRemoteApplicationEvent" + } + ] } ---- diff --git a/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/BusTraceEndpointTests.java b/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/BusTraceEndpointTests.java new file mode 100644 index 00000000..6d7916dc --- /dev/null +++ b/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/BusTraceEndpointTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present 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.cloud.bus; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.bus.endpoint.TraceBusEndpoint; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link TraceBusEndpoint}. + * + * @author Ngoc Nhan + */ +@SpringBootTest( + properties = { "management.endpoints.web.exposure.include=bustrace", "spring.cloud.bus.trace.enabled=true" }) +public class BusTraceEndpointTests { + + @Autowired(required = false) + private TraceBusEndpoint traceBusEndpoint; + + @Test + public void contextLoads() { + assertThat(this.traceBusEndpoint).isNotNull(); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class TestConfig { + + } + +} diff --git a/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/TraceListenerIntegrationTests.java b/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/TraceListenerIntegrationTests.java new file mode 100644 index 00000000..57784372 --- /dev/null +++ b/spring-cloud-bus-tests/src/test/java/org/springframework/cloud/bus/TraceListenerIntegrationTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-present 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.cloud.bus; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.rabbitmq.RabbitMQContainer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.boot.webtestclient.autoconfigure.AutoConfigureWebTestClient; +import org.springframework.cloud.bus.trace.Trace; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * Test for {@link org.springframework.cloud.bus.endpoint.TraceBusEndpoint}. + * + * @author Ngoc Nhan + */ +@Testcontainers +@SpringBootTest(webEnvironment = RANDOM_PORT, + properties = { "management.endpoints.web.exposure.include=busrefresh,bustrace", + "spring.cloud.bus.trace.enabled=true" }) +@AutoConfigureWebTestClient +public class TraceListenerIntegrationTests { + + @Container + @ServiceConnection + static RabbitMQContainer rabbitMQContainer = new RabbitMQContainer("rabbitmq:4.0-management"); + + @Test + void busTrace(@Autowired WebTestClient client) { + + client.post().uri("/actuator/busrefresh").exchange().expectStatus().is2xxSuccessful(); + BusTraceResponse response = client.get() + .uri("/actuator/bustrace") + .exchange() + .expectStatus() + .is2xxSuccessful() + .expectBody(BusTraceResponse.class) + .returnResult() + .getResponseBody(); + assertThat(response).isNotNull(); + assertThat(response.traces()).isNotEmpty().hasSize(2); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + static class TestConfig { + + } + + record BusTraceResponse(List traces) { + + } + +} diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/BusAutoConfiguration.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/BusAutoConfiguration.java index 05c49497..718469ae 100644 --- a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/BusAutoConfiguration.java +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/BusAutoConfiguration.java @@ -19,17 +19,19 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; -import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; 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.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.bus.endpoint.EnvironmentBusEndpoint; +import org.springframework.cloud.bus.endpoint.TraceBusEndpoint; import org.springframework.cloud.bus.event.Destination; import org.springframework.cloud.bus.event.EnvironmentChangeListener; import org.springframework.cloud.bus.event.PathDestinationFactory; import org.springframework.cloud.bus.event.TraceListener; +import org.springframework.cloud.bus.trace.InMemoryTraceRepository; +import org.springframework.cloud.bus.trace.TraceRepository; import org.springframework.cloud.context.environment.EnvironmentManager; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; @@ -68,14 +70,28 @@ public BusConsumer busConsumer(ApplicationEventPublisher applicationEventPublish @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Endpoint.class }) - @ConditionalOnBean(HttpExchangeRepository.class) @ConditionalOnProperty(BusProperties.PREFIX + ".trace.enabled") + @ConditionalOnAvailableEndpoint(TraceBusEndpoint.class) protected static class BusAckTraceConfiguration { @Bean @ConditionalOnMissingBean - public TraceListener ackTraceListener(HttpExchangeRepository repository) { - return new TraceListener(repository); + public TraceRepository traceRepository() { + return new InMemoryTraceRepository(); + } + + @Bean + @ConditionalOnBean(TraceRepository.class) + @ConditionalOnMissingBean + public TraceBusEndpoint traceBusEndpoint(TraceRepository traceRepository) { + return new TraceBusEndpoint(traceRepository); + } + + @Bean + @ConditionalOnBean(TraceRepository.class) + @ConditionalOnMissingBean + public TraceListener traceListener(TraceRepository traceRepository) { + return new TraceListener(traceRepository); } } diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/endpoint/TraceBusEndpoint.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/endpoint/TraceBusEndpoint.java new file mode 100644 index 00000000..8cd9b2f2 --- /dev/null +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/endpoint/TraceBusEndpoint.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present 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.cloud.bus.endpoint; + +import java.util.List; + +import org.springframework.boot.actuate.endpoint.OperationResponseBody; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.cloud.bus.trace.Trace; +import org.springframework.cloud.bus.trace.TraceRepository; +import org.springframework.util.Assert; + +/** + * {@link Endpoint @Endpoint} to expose {@link Trace} information. + * + * @author Ngoc Nhan + * @since 5.0.4 + */ +@Endpoint(id = "bustrace") +public class TraceBusEndpoint { + + private final TraceRepository repository; + + /** + * Create a new {@link TraceBusEndpoint} instance. + * @param repository the trace repository + */ + public TraceBusEndpoint(TraceRepository repository) { + Assert.notNull(repository, "'repository' must not be null"); + this.repository = repository; + } + + /** + * Description of an application's {@link Trace} entries. + */ + @ReadOperation + public TraceDescriptor busTrace() { + return new TraceDescriptor(this.repository.findAll()); + } + + public static final class TraceDescriptor implements OperationResponseBody { + + private final List traces; + + private TraceDescriptor(List traces) { + this.traces = traces; + } + + public List getTraces() { + return this.traces; + } + + } + +} diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/TraceListener.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/TraceListener.java index 3ff2fba6..bdba462e 100644 --- a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/TraceListener.java +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/event/TraceListener.java @@ -16,67 +16,70 @@ package org.springframework.cloud.bus.event; -import java.util.LinkedHashMap; -import java.util.Map; +import java.time.Clock; +import java.time.Instant; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository; -import org.springframework.context.event.EventListener; +import org.springframework.cloud.bus.trace.Trace; +import org.springframework.cloud.bus.trace.TraceRepository; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; /** - * A listener for sends and acks of remote application events. Inserts a record for each - * signal in the {@link HttpExchangeRepository}. + * A listener for application event sends and acks. Inserts a record for each signal into + * the {@link TraceRepository}. * * @author Dave Syer + * @author Ngoc Nhan */ -public class TraceListener { - - private static Log log = LogFactory.getLog(TraceListener.class); +public class TraceListener implements ApplicationListener { - private HttpExchangeRepository repository; + private final TraceRepository repository; - public TraceListener(HttpExchangeRepository repository) { + public TraceListener(TraceRepository repository) { this.repository = repository; } - @EventListener - public void onAck(AckRemoteApplicationEvent event) { - Map trace = getReceivedTrace(event); - // FIXME boot 2 this.repository.add(trace); - } + @Override + public void onApplicationEvent(ApplicationEvent event) { - @EventListener - public void onSend(SentApplicationEvent event) { - Map trace = getSentTrace(event); - // FIXME boot 2 this.repository.add(trace); - } + if (event instanceof AckRemoteApplicationEvent ackRemoteApplicationEvent) { + this.repository.add(getReceivedTrace(ackRemoteApplicationEvent)); + } - protected Map getSentTrace(SentApplicationEvent event) { - Map map = new LinkedHashMap(); - map.put("signal", "spring.cloud.bus.sent"); - map.put("type", event.getType().getSimpleName()); - map.put("id", event.getId()); - map.put("origin", event.getOriginService()); - map.put("destination", event.getDestinationService()); - if (log.isDebugEnabled()) { - log.debug(map); + if (event instanceof SentApplicationEvent sentApplicationEvent) { + this.repository.add(getSentTrace(sentApplicationEvent)); } - return map; + } - protected Map getReceivedTrace(AckRemoteApplicationEvent event) { - Map map = new LinkedHashMap(); - map.put("signal", "spring.cloud.bus.ack"); - map.put("event", event.getEvent().getSimpleName()); - map.put("id", event.getAckId()); - map.put("origin", event.getOriginService()); - map.put("destination", event.getAckDestinationService()); - if (log.isDebugEnabled()) { - log.debug(map); - } - return map; + /** + * Creates a trace for a acks application event. + * @param event the acks application event + * @return the trace for the acks application event + */ + protected Trace getReceivedTrace(AckRemoteApplicationEvent event) { + + Trace trace = new Trace(Instant.now(Clock.systemUTC()), "spring.cloud.bus.ack", + event.getEvent().getSimpleName()); + trace.setId(event.getAckId()); + trace.setOrigin(event.getOriginService()); + trace.setDestination(event.getAckDestinationService()); + return trace; + } + + /** + * Creates a trace for a sent application event. + * @param event the sent application event + * @return the trace for the sent application event + */ + protected Trace getSentTrace(SentApplicationEvent event) { + + Trace trace = new Trace(Instant.now(Clock.systemUTC()), "spring.cloud.bus.sent", + event.getType().getSimpleName()); + trace.setId(event.getId()); + trace.setOrigin(event.getOriginService()); + trace.setDestination(event.getDestinationService()); + return trace; } } diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/InMemoryTraceRepository.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/InMemoryTraceRepository.java new file mode 100644 index 00000000..609acf25 --- /dev/null +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/InMemoryTraceRepository.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-present 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.cloud.bus.trace; + +import java.util.LinkedList; +import java.util.List; + +/** + * In-memory implementation of {@link TraceRepository}. + * + * @author Ngoc Nhan + * @since 5.0.4 + */ +public class InMemoryTraceRepository implements TraceRepository { + + private final List traces = new LinkedList<>(); + + private int capacity = 100; + + private boolean reverse = true; + + /** + * Flag to say that the repository lists exchanges in reverse order. + * @param reverse flag value (default true) + */ + public void setReverse(boolean reverse) { + synchronized (this.traces) { + this.reverse = reverse; + } + } + + /** + * Set the capacity of the in-memory repository. + * @param capacity the capacity + */ + public void setCapacity(int capacity) { + synchronized (this.traces) { + this.capacity = capacity; + } + } + + @Override + public List findAll() { + synchronized (this.traces) { + return List.copyOf(this.traces); + } + } + + @Override + public void add(Trace trace) { + synchronized (this.traces) { + while (this.traces.size() >= this.capacity) { + this.traces.remove(this.reverse ? this.capacity - 1 : 0); + } + if (this.reverse) { + this.traces.add(0, trace); + } + else { + this.traces.add(trace); + } + } + } + +} diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/Trace.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/Trace.java new file mode 100644 index 00000000..c0af7aea --- /dev/null +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/Trace.java @@ -0,0 +1,152 @@ +/* + * Copyright 2012-present 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.cloud.bus.trace; + +import java.time.Instant; +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; + +/** + * A trace of application event sends and acks. Data from this class is exposed by the + * {@link org.springframework.cloud.bus.endpoint.TraceBusEndpoint}, usually as JSON. + * + * @author Ngoc Nhan + * @since 5.0.4 + */ +public final class Trace { + + private final Instant timestamp; + + private final String signal; + + private final String type; + + private @Nullable String id; + + private @Nullable String origin; + + private @Nullable String destination; + + /** + * Primarily for use by {@link TraceRepository} implementations when storing a trace. + * @param timestamp the instant that the trace was created (must not be {@code null}) + * @param signal the signal (must not be {@code null} or empty) + * @param type the event type (must not be {@code null} or empty) + */ + public Trace(Instant timestamp, String signal, String type) { + Assert.notNull(timestamp, "timestamp must not be null"); + Assert.hasText(signal, "signal must not be null or empty"); + Assert.hasText(type, "type must not be null or empty"); + this.timestamp = timestamp; + this.signal = signal; + this.type = type; + } + + /** + * Returns the timestamp of the trace. + * @return the trace timestamp + */ + public Instant getTimestamp() { + return this.timestamp; + } + + /** + * Returns the signal associated with the trace. + * @return the trace signal + */ + public String getSignal() { + return signal; + } + + /** + * Returns the event type associated with the trace. + * @return the event type + */ + public String getType() { + return this.type; + } + + /** + * Returns the event id associated with the trace. + * @return the event id + */ + public @Nullable String getId() { + return this.id; + } + + /** + * Sets the event ID associated with the trace. + * @param id the event ID + */ + public void setId(@Nullable String id) { + this.id = id; + } + + /** + * Returns the origin service associated with the trace. + * @return the origin service + */ + public @Nullable String getOrigin() { + return this.origin; + } + + /** + * Sets the origin service associated with the trace. + * @param origin the origin service + */ + public void setOrigin(@Nullable String origin) { + this.origin = origin; + } + + /** + * Returns the destination service associated with the trace. + * @return the destination service + */ + public @Nullable String getDestination() { + return this.destination; + } + + /** + * Sets the destination service associated with the trace. + * @param destination the destination service + */ + public void setDestination(@Nullable String destination) { + this.destination = destination; + } + + @Override + public boolean equals(Object o) { + + if (!(o instanceof Trace that)) { + return false; + } + + return Objects.equals(this.timestamp, that.timestamp) && Objects.equals(this.signal, that.signal) + && Objects.equals(this.type, that.type) && Objects.equals(this.id, that.id) + && Objects.equals(this.origin, that.origin) && Objects.equals(this.destination, that.destination); + } + + @Override + public int hashCode() { + + return Objects.hash(this.timestamp, this.signal, this.type, this.id, this.origin, this.destination); + } + +} diff --git a/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/TraceRepository.java b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/TraceRepository.java new file mode 100644 index 00000000..42006d35 --- /dev/null +++ b/spring-cloud-bus/src/main/java/org/springframework/cloud/bus/trace/TraceRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-present 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.cloud.bus.trace; + +import java.util.List; + +/** + * A repository for {@link Trace} instances. + * + * @author Ngoc Nhan + * @since 5.0.4 + */ +public interface TraceRepository { + + /** + * Find all {@link Trace} instances contained in the repository. + * @return all contained traces + */ + List findAll(); + + /** + * Adds an {@link Trace} instance to the repository. + * @param trace the trace to add + */ + void add(Trace trace); + +} diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java index ae0a24ce..f9910266 100644 --- a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java +++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/BusAutoConfigurationTests.java @@ -34,7 +34,9 @@ import org.springframework.cloud.bus.event.SentApplicationEvent; import org.springframework.cloud.bus.event.ShutdownListener; import org.springframework.cloud.bus.event.ShutdownRemoteApplicationEvent; +import org.springframework.cloud.bus.event.TraceListener; import org.springframework.cloud.bus.event.UnknownRemoteApplicationEvent; +import org.springframework.cloud.bus.trace.TraceRepository; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; import org.springframework.cloud.stream.config.BindingProperties; @@ -317,6 +319,20 @@ public void serviceMatcherIdIsConstantAfterRefresh() { assertThat(newServiceId).isEqualTo(originalServiceId); } + @Test + public void traceRepositoryBeanShouldBeAvailable() { + + AckRemoteApplicationEvent event = new AckRemoteApplicationEvent(this, "foo", + new PathDestinationFactory().getDestination(null), "ID", "bar", RemoteApplicationEvent.class); + this.context = SpringApplication.run(new Class[] { TraceConfiguration.class }, new String[] { + "--spring.cloud.bus.trace.enabled=true", "--management.endpoints.web.exposure.include=bustrace" }); + this.context.publishEvent(event); + + assertThat(context.getBean(TraceListener.class)).isNotNull(); + assertThat(context.getBean(TraceRepository.class)).isNotNull(); + assertThat(context.getBean(TraceRepository.class).findAll()).hasSize(1); + } + @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration protected static class RefreshConfig { @@ -432,4 +448,12 @@ public void onApplicationEvent(AckRemoteApplicationEvent event) { } + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @ImportAutoConfiguration({ BusAutoConfiguration.class, TestChannelBinderConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) + protected static class TraceConfiguration { + + } + } diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/endpoint/TraceBusEndpointTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/endpoint/TraceBusEndpointTests.java new file mode 100644 index 00000000..17e53374 --- /dev/null +++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/endpoint/TraceBusEndpointTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present 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.cloud.bus.endpoint; + +import java.time.Instant; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.bus.trace.InMemoryTraceRepository; +import org.springframework.cloud.bus.trace.Trace; +import org.springframework.cloud.bus.trace.TraceRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link TraceBusEndpoint}. + * + * @author Ngoc Nhan + */ +class TraceBusEndpointTests { + + @Test + void busTrace() { + + Trace trace = new Trace(Instant.now(), "spring.cloud.bus.ack", "type"); + trace.setId("id"); + trace.setOrigin("origin"); + trace.setDestination("destination"); + + TraceRepository repository = new InMemoryTraceRepository(); + repository.add(trace); + List traces = new TraceBusEndpoint(repository).busTrace().getTraces(); + assertThat(traces).hasSize(1); + + Trace busTrace = traces.get(0); + assertThat(busTrace.getSignal()).isEqualTo("spring.cloud.bus.ack"); + } + +} diff --git a/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/trace/InMemoryTraceRepositoryTests.java b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/trace/InMemoryTraceRepositoryTests.java new file mode 100644 index 00000000..d83a5f0c --- /dev/null +++ b/spring-cloud-bus/src/test/java/org/springframework/cloud/bus/trace/InMemoryTraceRepositoryTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-present 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.cloud.bus.trace; + +import java.time.Instant; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link InMemoryTraceRepository}. + * + * @author Ngoc Nhan + */ +public class InMemoryTraceRepositoryTests { + + private final InMemoryTraceRepository repository = new InMemoryTraceRepository(); + + @Test + void adWhenHasLimitedCapacityRestrictsSize() { + + this.repository.setCapacity(2); + + for (Trace trace : this.createTraces()) { + this.repository.add(trace); + } + List trace = this.repository.findAll(); + + assertThat(trace).hasSize(2); + assertThat(trace.get(0).getType()).isEqualTo("type3"); + assertThat(trace.get(1).getType()).isEqualTo("type2"); + } + + @Test + void addWhenReverseFalseReturnsInCorrectOrder() { + + this.repository.setReverse(false); + this.repository.setCapacity(2); + + for (Trace trace : this.createTraces()) { + this.repository.add(trace); + } + List trace = this.repository.findAll(); + + assertThat(trace).hasSize(2); + assertThat(trace.get(0).getType()).isEqualTo("type2"); + assertThat(trace.get(1).getType()).isEqualTo("type3"); + } + + private List createTraces() { + + Trace trace1 = new Trace(Instant.now(), "spring.cloud.bus.ack", "type1"); + trace1.setId("id1"); + trace1.setOrigin("origin1"); + trace1.setDestination("destination1"); + + Trace trace2 = new Trace(Instant.now(), "spring.cloud.bus.sent", "type2"); + trace2.setId("id2"); + trace2.setOrigin("origin2"); + trace2.setDestination("destination2"); + + Trace trace3 = new Trace(Instant.now(), "spring.cloud.bus.ack", "type3"); + trace3.setId("id3"); + trace3.setOrigin("origin3"); + trace3.setDestination("destination3"); + return List.of(trace1, trace2, trace3); + } + +}