Use JSON for AMQP Hystrix data
Fixes gh-126
This commit is contained in:
@@ -47,6 +47,10 @@
|
||||
<artifactId>spring-integration-java-dsl</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.hystrix</groupId>
|
||||
<artifactId>hystrix-core</artifactId>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package org.springframework.netflix.hystrix.amqp;
|
||||
|
||||
import com.netflix.hystrix.HystrixCircuitBreaker;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.amqp.core.DirectExchange;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
@@ -16,50 +18,68 @@ import org.springframework.integration.dsl.IntegrationFlows;
|
||||
import org.springframework.integration.dsl.amqp.Amqp;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.netflix.hystrix.HystrixCircuitBreaker;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass({HystrixCircuitBreaker.class, AmqpTemplate.class})
|
||||
@ConditionalOnClass({ HystrixCircuitBreaker.class, RabbitTemplate.class })
|
||||
@ConditionalOnExpression("${hystrix.stream.amqp.enabled:true}")
|
||||
@IntegrationComponentScan(basePackageClasses = HystrixStreamChannel.class)
|
||||
@EnableScheduling
|
||||
public class HystrixStreamAutoConfiguration {
|
||||
@Configuration
|
||||
@ConditionalOnExpression("${hystrix.stream.amqp.enabled:true}")
|
||||
@IntegrationComponentScan(basePackageClasses = HystrixStreamChannel.class)
|
||||
@EnableScheduling
|
||||
protected static class HystrixStreamAmqpAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private AmqpTemplate amqpTemplate;
|
||||
@Autowired
|
||||
private RabbitTemplate amqpTemplate;
|
||||
|
||||
@Bean
|
||||
public HystrixStreamTask hystrixStreamTask() {
|
||||
return new HystrixStreamTask();
|
||||
}
|
||||
@Autowired(required = false)
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Bean
|
||||
public DirectChannel hystrixStream() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DirectExchange hystrixStreamExchange() {
|
||||
DirectExchange exchange = new DirectExchange(Constants.HYSTRIX_STREAM_NAME);
|
||||
return exchange;
|
||||
}
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Jackson2JsonMessageConverter converter = messageConverter();
|
||||
amqpTemplate.setMessageConverter(converter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow hystrixStreamOutboundFlow() {
|
||||
return IntegrationFlows.from("hystrixStream")
|
||||
//TODO: set content type
|
||||
/*.enrichHeaders(new ComponentConfigurer<HeaderEnricherSpec>() {
|
||||
@Override
|
||||
public void configure(HeaderEnricherSpec spec) {
|
||||
spec.header("content-type", "application/json", true);
|
||||
}
|
||||
})*/
|
||||
.handle(Amqp.outboundAdapter(this.amqpTemplate).exchangeName(Constants.HYSTRIX_STREAM_NAME))
|
||||
.get();
|
||||
}
|
||||
}
|
||||
@Bean
|
||||
public HystrixStreamTask hystrixStreamTask() {
|
||||
return new HystrixStreamTask();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DirectChannel hystrixStream() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DirectExchange hystrixStreamExchange() {
|
||||
DirectExchange exchange = new DirectExchange(Constants.HYSTRIX_STREAM_NAME);
|
||||
return exchange;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow hystrixStreamOutboundFlow() {
|
||||
return IntegrationFlows
|
||||
.from("hystrixStream")
|
||||
// TODO: set content type
|
||||
/*
|
||||
* .enrichHeaders(new ComponentConfigurer<HeaderEnricherSpec>() {
|
||||
*
|
||||
* @Override public void configure(HeaderEnricherSpec spec) {
|
||||
* spec.header("content-type", "application/json", true); } })
|
||||
*/
|
||||
.handle(Amqp.outboundAdapter(this.amqpTemplate).exchangeName(
|
||||
Constants.HYSTRIX_STREAM_NAME)).get();
|
||||
}
|
||||
|
||||
private Jackson2JsonMessageConverter messageConverter() {
|
||||
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
|
||||
if (objectMapper != null) {
|
||||
converter.setJsonObjectMapper(objectMapper);
|
||||
}
|
||||
return converter;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.springframework.netflix.turbine.amqp;
|
||||
package org.springframework.cloud.netflix.turbine.amqp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.springframework.netflix.turbine.amqp;
|
||||
package org.springframework.cloud.netflix.turbine.amqp;
|
||||
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.springframework.cloud.netflix.turbine.amqp;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.core.Binding;
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.DirectExchange;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.cloud.netflix.Constants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.dsl.IntegrationFlow;
|
||||
import org.springframework.integration.dsl.IntegrationFlows;
|
||||
import org.springframework.integration.dsl.amqp.Amqp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(AmqpTemplate.class)
|
||||
@ConditionalOnExpression("${turbine.amqp.enabled:true}")
|
||||
public class TurbineAmqpAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private ConnectionFactory connectionFactory;
|
||||
|
||||
@Autowired
|
||||
private RabbitTemplate amqpTemplate;
|
||||
|
||||
@Autowired(required = false)
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
Jackson2JsonMessageConverter converter = messageConverter();
|
||||
amqpTemplate.setMessageConverter(converter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DirectExchange hystrixStreamExchange() {
|
||||
DirectExchange exchange = new DirectExchange(Constants.HYSTRIX_STREAM_NAME);
|
||||
return exchange;
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Binding localTurbineAmqpQueueBinding() {
|
||||
return BindingBuilder.bind(hystrixStreamQueue()).to(hystrixStreamExchange())
|
||||
.with("");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue hystrixStreamQueue() {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("x-message-ttl", 60000); // TODO: configure TTL
|
||||
Queue queue = new Queue(Constants.HYSTRIX_STREAM_NAME, false, false, false, args);
|
||||
return queue;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow hystrixStreamAggregatorInboundFlow() {
|
||||
return IntegrationFlows
|
||||
.from(Amqp.inboundAdapter(connectionFactory, hystrixStreamQueue())
|
||||
.messageConverter(messageConverter()))
|
||||
.channel("hystrixStreamAggregator").get();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Aggregator hystrixStreamAggregator() {
|
||||
return new Aggregator();
|
||||
}
|
||||
|
||||
private Jackson2JsonMessageConverter messageConverter() {
|
||||
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
|
||||
if (objectMapper != null) {
|
||||
converter.setJsonObjectMapper(objectMapper);
|
||||
}
|
||||
return converter;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +1,40 @@
|
||||
package org.springframework.netflix.turbine.amqp;
|
||||
package org.springframework.cloud.netflix.turbine.amqp;
|
||||
|
||||
import com.netflix.turbine.aggregator.InstanceKey;
|
||||
import com.netflix.turbine.aggregator.StreamAggregator;
|
||||
import com.netflix.turbine.internal.JsonUtility;
|
||||
import static io.reactivex.netty.pipeline.PipelineConfigurators.sseServerConfigurator;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.reactivex.netty.RxNetty;
|
||||
import io.reactivex.netty.protocol.http.server.HttpServer;
|
||||
import io.reactivex.netty.protocol.text.sse.ServerSentEvent;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import rx.Observable;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static io.reactivex.netty.pipeline.PipelineConfigurators.sseServerConfigurator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import rx.Observable;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
import com.netflix.turbine.aggregator.InstanceKey;
|
||||
import com.netflix.turbine.aggregator.StreamAggregator;
|
||||
import com.netflix.turbine.internal.JsonUtility;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@ConfigurationProperties("turbine.amqp")
|
||||
@EnableConfigurationProperties(TurbineAmqpProperties.class)
|
||||
public class TurbineAmqpConfiguration implements SmartLifecycle {
|
||||
|
||||
private boolean running = false;
|
||||
|
||||
private int port = 8989;
|
||||
@Autowired
|
||||
private TurbineAmqpProperties turbine;
|
||||
|
||||
@Bean
|
||||
public PublishSubject<Map<String, Object>> hystrixSubject() {
|
||||
@@ -39,19 +44,26 @@ public class TurbineAmqpConfiguration implements SmartLifecycle {
|
||||
@Bean
|
||||
public HttpServer<ByteBuf, ServerSentEvent> aggregatorServer() {
|
||||
// multicast so multiple concurrent subscribers get the same stream
|
||||
Observable<Map<String, Object>> publishedStreams = StreamAggregator.aggregateGroupedStreams(hystrixSubject()
|
||||
.groupBy(data -> InstanceKey.create((String) data.get("instanceId"))))
|
||||
.doOnUnsubscribe(() -> log.info("AmqpTurbine => Unsubscribing aggregation."))
|
||||
.doOnSubscribe(() -> log.info("AmqpTurbine => Starting aggregation"))
|
||||
.flatMap(o -> o).publish().refCount();
|
||||
Observable<Map<String, Object>> publishedStreams = StreamAggregator
|
||||
.aggregateGroupedStreams(
|
||||
hystrixSubject().groupBy(
|
||||
data -> InstanceKey.create((String) data
|
||||
.get("instanceId"))))
|
||||
.doOnUnsubscribe(() -> log.info("Unsubscribing aggregation."))
|
||||
.doOnSubscribe(() -> log.info("Starting aggregation")).flatMap(o -> o)
|
||||
.publish().refCount();
|
||||
|
||||
HttpServer<ByteBuf, ServerSentEvent> httpServer = RxNetty.createHttpServer(port, (request, response) -> {
|
||||
log.info("AmqpTurbine => SSE Request Received");
|
||||
response.getHeaders().setHeader("Content-Type", "text/event-stream");
|
||||
return publishedStreams
|
||||
.doOnUnsubscribe(() -> log.info("AmqpTurbine => Unsubscribing RxNetty server connection"))
|
||||
.flatMap(data -> response.writeAndFlush(new ServerSentEvent(null, null, JsonUtility.mapToJson(data))));
|
||||
}, sseServerConfigurator());
|
||||
HttpServer<ByteBuf, ServerSentEvent> httpServer = RxNetty.createHttpServer(
|
||||
turbine.getPort(),
|
||||
(request, response) -> {
|
||||
log.info("SSE Request Received");
|
||||
response.getHeaders().setHeader("Content-Type", "text/event-stream");
|
||||
return publishedStreams.doOnUnsubscribe(
|
||||
() -> log.info("Unsubscribing RxNetty server connection"))
|
||||
.flatMap(
|
||||
data -> response.writeAndFlush(new ServerSentEvent(
|
||||
null, null, JsonUtility.mapToJson(data))));
|
||||
}, sseServerConfigurator());
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
@@ -75,7 +87,8 @@ public class TurbineAmqpConfiguration implements SmartLifecycle {
|
||||
public void stop() {
|
||||
try {
|
||||
aggregatorServer().shutdown();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
log.error("Error shutting down", e);
|
||||
}
|
||||
running = false;
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.springframework.cloud.netflix.turbine.amqp;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@ConfigurationProperties("turbine.amqp")
|
||||
@Data
|
||||
public class TurbineAmqpProperties {
|
||||
|
||||
private int port = 8989;
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.springframework.netflix.turbine.amqp;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.amqp.core.Binding;
|
||||
import org.springframework.amqp.core.BindingBuilder;
|
||||
import org.springframework.amqp.core.DirectExchange;
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.cloud.netflix.Constants;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.dsl.IntegrationFlow;
|
||||
import org.springframework.integration.dsl.IntegrationFlows;
|
||||
import org.springframework.integration.dsl.amqp.Amqp;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(AmqpTemplate.class)
|
||||
public class TurbineAmqpAutoConfiguration {
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnExpression("${turbine.amqp.enabled:true}")
|
||||
protected static class HystrixStreamAggregatorAutoConfiguration {
|
||||
|
||||
@Autowired
|
||||
private ConnectionFactory connectionFactory;
|
||||
|
||||
//TODO: how to fail gracefully if no rabbit?
|
||||
@Bean
|
||||
public DirectExchange hystrixStreamExchange() {
|
||||
DirectExchange exchange = new DirectExchange(Constants.HYSTRIX_STREAM_NAME);
|
||||
return exchange;
|
||||
}
|
||||
|
||||
@Bean
|
||||
protected Binding localTurbineAmqpQueueBinding() {
|
||||
return BindingBuilder.bind(hystrixStreamQueue()).to(hystrixStreamExchange()).with("");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Queue hystrixStreamQueue() {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("x-message-ttl", 60000); //TODO: configure TTL
|
||||
Queue queue = new Queue(Constants.HYSTRIX_STREAM_NAME, false, false, false, args);
|
||||
return queue;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IntegrationFlow hystrixStreamAggregatorInboundFlow() {
|
||||
return IntegrationFlows.from(Amqp.inboundAdapter(connectionFactory, hystrixStreamQueue()))
|
||||
.channel("hystrixStreamAggregator")
|
||||
.get();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Aggregator hystrixStreamAggregator() {
|
||||
return new Aggregator();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.netflix.turbine.amqp.TurbineAmqpAutoConfiguration
|
||||
org.springframework.cloud.netflix.turbine.amqp.TurbineAmqpAutoConfiguration
|
||||
|
||||
@@ -12,8 +12,7 @@ import com.netflix.turbine.internal.JsonUtility;
|
||||
|
||||
import rx.Observable;
|
||||
import rx.observables.GroupedObservable;
|
||||
|
||||
import static org.springframework.netflix.turbine.amqp.Aggregator.getPayloadData;
|
||||
import static org.springframework.cloud.netflix.turbine.amqp.Aggregator.getPayloadData;
|
||||
|
||||
public class AggregatorTest {
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.springframework.netflix.turbine.amqp;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.netflix.turbine.amqp.EnableTurbineAmqp;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
|
||||
Reference in New Issue
Block a user