Spring Boot, Sleuth, OTEL, and Honeycomb - spring-boot

I have a scenario where I have Spring Boot integrated with OTEL and shipping to Honeycomb.io. I am trying to add an environment tag to each trace. I have created a class:
#Component
public class EnvironmentSpanProcessor implements SpanProcessor {
#Value("${ENVIRONMENT")
private String environment;
Queue<SpanData> spans = new LinkedBlockingQueue<>(50);
#Override
public void onStart(Context context, ReadWriteSpan readWriteSpan) {
readWriteSpan.setAttribute("env", environment);
}
#Override
public boolean isStartRequired() {
return false;
}
#Override
public void onEnd(ReadableSpan readableSpan) {
this.spans.add(readableSpan.toSpanData());
}
#Override
public boolean isEndRequired() {
return true;
}
}
I have set break points in this class, and they never hit on startup, even though the bean can be seen in actuator. I have put breakpoints on:
SdkTracerProvider otelTracerProvider(SpanLimits spanLimits, ObjectProvider<List<SpanProcessor>> spanProcessors,
SpanExporterCustomizer spanExporterCustomizer, ObjectProvider<List<SpanExporter>> spanExporters,
Sampler sampler, Resource resource, SpanProcessorProvider spanProcessorProvider) {
SdkTracerProviderBuilder sdkTracerProviderBuilder = SdkTracerProvider.builder().setResource(resource)
.setSampler(sampler).setSpanLimits(spanLimits);
List<SpanProcessor> processors = spanProcessors.getIfAvailable(ArrayList::new);
processors.addAll(spanExporters.getIfAvailable(ArrayList::new).stream()
.map(e -> spanProcessorProvider.toSpanProcessor(spanExporterCustomizer.customize(e)))
.collect(Collectors.toList()));
processors.forEach(sdkTracerProviderBuilder::addSpanProcessor);
return sdkTracerProviderBuilder.build();
}
in OtelAutoConfiguration and am not seeing them firing either on startup.
My pom.xml relevant section is:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-brave</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-sleuth-otel-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-extension-trace-propagators</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.47.0</version>
</dependency>
And my configuration from application.yaml
sleuth:
enabled: true
web:
additional-skip-pattern: /readiness|/liveness
client.skip-pattern: /readiness
sampler:
probability: 1.0
rate: 100
propagation:
type: OT_TRACER
otel:
config:
trace-id-ratio-based: 1.0
log.exporter.enabled: true
exporter:
otlp:
endpoint: https://api.honeycomb.io
headers:
x-honeycomb-team: ${TELEMETRY_API_KEY}
x-honeycomb-dataset: app-telemetry
sleuth-span-filter:
enabled: true
resource:
enabled: true
I am getting traces, so it appears the system itself is working, however I cannot get my env tag added.
Preemptive thank you to #marcingrzejszczak for the help so far on my gist: https://gist.github.com/fpmoles/b880ccfdef2d2138169ed398e87ec396

I'm unsure why your span processor is not being picked up by Spring and being added to your list of processors being registered with the tracer provider.
An alternative way to set process consistent values, like environment, would be to set it as a resource attribute. This is more desireable because it's set once and delivered once per batch of spans sent to the configured backend (eg Honeycomb). Using a span processor adds the same attribute to every span.
This can be done in a few different ways:
If using AutoConfigure, you can set via system property or environment variable
Set directly on the resource during your otelTracerProvider method:
resource.setAttribute("environment", "${environment}");
FYI Honeycomb has OTel Java SDK & Agent distros to help simplify sending data that reduces required configuration and sets sensible defaults.

Related

Micrometer StackdriverMeterRegistry only publishes custom metrics to GCP Monitoring, not automatic instrumentation metrics?

I have this example https://quarkus.io/guides/micrometer (micrometer quickstart directory) running which uses Quarkus and Micrometer together. The example uses Prometheus as the MeterRegistry but I changed it to use the StackdriverMeterRegistry in hopes the same auto instrumentation that shows up in Prometheus would show up in Google Cloud Monitoring.
However, I only see the custom metrics I made appear into Google Cloud Monitoring, and not the auto instrumentation provided by micrometer.
I am unsure if I should think that this is just an issue with the Micrometer StackdriverMeterRegistry library itself or if I am doing something wrong. Any guidance is appreciated.
Code changes:
// Update the constructor to create the gauge
ExampleResource(MeterRegistry registry) {
/* Code for micrometer */
StackdriverConfig stackdriverConfig = new StackdriverConfig() {
#Override
public String projectId() {
return "projectId";
}
#Override
public String get(String key) {
return null;
}
};
this.registry = StackdriverMeterRegistry.builder(stackdriverConfig).build();
registry.config().commonTags("application", "projectId");
registry.gaugeCollectionSize("example.list.size", Tags.empty(), list);
}
Added to pom.xml
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-stackdriver</artifactId>
</dependency>
After tinkering and speaking directly with the Micrometer team I found out the issue. The documentation is a bit confusing but I had imported the StackDriver extension wrong and the default registry being used for the quarkus project was getting all the auto instrumentation but not the StackDriver one. So this default registry needed to be changed to the StackDriver one.
I have uploaded a basic example of using Quarkus StackDriver and Micrometer together using the basic example found on the Micrometer Quarkus documentation page.
https://github.com/jayleenli/quarkus-micrometer-stackdriver-quickstart
The changes:
Add to pom.xml
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.micrometer.registry</groupId>
<artifactId>quarkus-micrometer-registry-stackdriver</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>1.7.3</version>
</dependency>
Then add some Quarkus properties, I used application.properties but there are other ways you can do this.
application.properties
quarkus.micrometer.export.stackdriver.enabled=true
quarkus.micrometer.export.stackdriver.default-registry=true
quarkus.micrometer.export.stackdriver.project-id=fake-id
quarkus.micrometer.export.stackdriver.publish=true
quarkus.micrometer.export.stackdriver.resource-type=global
quarkus.micrometer.export.stackdriver.step=1m
In main class
#Path("/")
public class ExampleResource {
#ConfigProperty(name = "quarkus.micrometer.export.stackdriver.enabled")
boolean enabled;
#ConfigProperty(name = "quarkus.micrometer.export.stackdriver.default-registry")
boolean export;
#ConfigProperty(name="quarkus.micrometer.export.stackdriver.project-id")
String projectId;
#ConfigProperty(name="quarkus.micrometer.export.stackdriver.publish")
boolean publish;
#ConfigProperty(name="quarkus.micrometer.export.stackdriver.resource-type")
String resourceType;
#ConfigProperty(name="quarkus.micrometer.export.stackdriver.step")
String step;

Load balancer does not contain an instance for the service

I want to use Eureka client with spring-cloud-starter-loadbalancer. But when I added configuration I get error. When I remove #LoadBalancerClient(name = "mail-service", configuration = LoadBalancerConfiguration.class) and LoadBalancerConfiguration class configuration it's working fine. I tried this code:
#FeignClient(name = "mail-service")
#LoadBalancerClient(name = "mail-service", configuration = LoadBalancerConfiguration.class)
public interface EmailClient {
#RequestMapping(method = RequestMethod.POST, value = "/engine/emails/register")
void setUserRegistration(CreateUserDTO createUserDTO);
}
#Configuration
public class LoadBalancerConfiguration {
#Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withBlockingDiscoveryClient()
.withSameInstancePreference()
.withHealthChecks()
.build(context);
}
}
application.yml:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_URI:http://localhost:8761/eureka}
fetchRegistry: true
healthcheck:
enabled: true
instance:
preferIpAddress: true
leaseRenewalIntervalInSeconds: 10
POM.xml dependencies
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
I get this error when I have only one target service.
[503] during [POST] to [http://mail-service/engine/emails/register] [EmailClient#setUserRegistration(CreateUserDTO)]: [Load balancer does not contain an instance for the service mail-service]
I use Release Train Version: 2020.0.3
Do you know what could be the problem?
Any application using load balancer should follow the below order
Start the Eureka Server
Start the instances of the Service one by one which have dependency
Any application relies on information from a service registry (i.e. Eureka). Until the application is registered with it's instances by the serviceId , the Eureka server will not be able to pick that instance while load-balancing.
In the code you have shared, the bean ServiceInstanceListSupplier is created in the configuration class along with the health checks ( .withHealthChecks() ). This is causing the application to fail as service has not been registered yet.
Also, the LoadBalancer config should not be in a #Configuration-annotated class instead, it should be a class passed for config via #LoadBalancerClient or #LoadBalancerClients annotation, as described here.
The only bean you need to instantiate is the ServiceInstanceListSupplier (if you add spring-cloud-starter-loadbalancer, LoadBalancerClientFactory etc. will be instantiated by the starter itself).
So your LoadBalancer configuration class should look like code below.
It should not be in the #Configuration class:
public class LoadBalancerConfiguration {
#Bean
public ServiceInstanceListSupplier instanceSupplier(ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHealthChecks()
.build(context);
}
}
As explained in this link, the actual #Configuration class , will have the following annotation: #LoadBalancerClients(defaultConfiguration = LoadBalancerConfiguration .class).
Then, if you enable health-checks in the instances, it should work without any issues.

multi output binding in spring cloud data flow

I'm trying to setup a multi-destination bindings but for some reason, messages from the second channel are going to the first exchange.queue. For example:
spring:
cloud:
stream:
bindings:
output:
destination: exchange1
producer.requiredGroups: queue1
output-other:
destination: exchange2
producer.requiredGroups: queue2
I also used org.springframework.cloud.stream.messaging.Source for the default Output and created a dedicated Source Binding for output-other channel
public interface OtherSource {
String OUTPUT = "output-other";
#Output(OtherSource.OUTPUT)
MessageChannel output();
}
and the producer class
#EnableBinding(Source.class)
public class OutputSender {
private final Source source;
public void send(Output1 obj) {
Message message = toMessage(obj);
this.source.output().send(message);
}
}
this works as expected. messages are sent to the correct queue (exchange1.queue1)
second producer:
#EnableBinding(OtherSource.class)
public class OutputOtherSender {
OtherSource source;
public void send(Output2 obj) {
Message message = toMessage(obj)
this.source.output().send(obj);
}
}
2 issues with this setup:
exchange2.queue2 is not created (something wrong with application.yml configuration?)
messages that are sent using OtherSource are going to exchange1.queue1
Dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
By default, the stream applications in Spring Cloud Data Flow are linear, meaning the applications are bound to one another using single output->single input.
If you want to have your stream using multiple input/output destinations, then you have to manually configure your bindings(using the Spring Cloud Stream properties) and define your application as app type - a special type for a streaming app in SCDF that lets the user configure the bindings manually.
For more information on this, you can refer: https://dataflow.spring.io/docs/feature-guides/streams/stream-application-dsl/

Object distortion while passing from REST service to Spring app

I've got strange problem and I hope you will to help me to solve it.
I try to pass list of objects, where each object contains LocalDate parameter (JodaTime library) from test service to my controller.
This is method from my service. It returns list of objects. Look at the dates printed out in the loop.
#RequestMapping("/getListaRecept")
#ResponseBody
public ListaRecept sendAnswer(){
ListaRecept listaReceptFiltered = prescriptionCreator.createListaRecept();
for(Recepta r : listaReceptFiltered.getListaRecept()){
System.out.println(r.toString());
}
return listaReceptFiltered;
}
Dates are correct
Recepta{id=3, nazwa='nurofen', status=NOT_REALIZED, date=2017-07-27}
Recepta{id=1, nazwa='ibuprom', status=ANNULED, date=2014-12-25}
Recepta{id=2, nazwa='apap', status=REALIZED, date=2016-08-18}
And now I'm invoking this method from my SpringBoot app using restTemplate. And then received list is printed out
private final RestTemplate restTemplate;
public SgrService2(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
this.restTemplate.getMessageConverters()
.add(0, new StringHttpMessageConverter(Charset.forName("UTF-16")));
}
public ListaRecept getList() {
for(Recepta r : this.restTemplate.getForObject("http://localhost:8090/getListaRecept",
ListaRecept.class).getListaRecept()){
System.out.println(r.toString());
}
return this.restTemplate.getForObject("http://localhost:8090/getListaRecept",
ListaRecept.class);
}
As you can see all dates were replaced with current date :/
Recepta{id=3, nazwa='nurofen', status=NOT_REALIZED, date=2017-09-30}
Recepta{id=1, nazwa='ibuprom', status=ANNULED, date=2017-09-30}
Recepta{id=2, nazwa='apap', status=REALIZED, date=2017-09-30}
I have no idea what is going on...
Here you have pom dependencies
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.9.9</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.1</version>
</dependency>
Thank you in advance for your help
It seems to me that you are using the wrong jackson module, instead of jsr310 (which I guess is for Java 8 date types), try using the artifact jackson-datatype-joda and register the module JodaModule.

Spring Cloud Feign/Ribbon with corporate proxy

I want to consume a REST service from the outside world behind a corporate proxy with authentication.
How do I configure Spring Boot + Spring Cloud Feign/Ribbon to use our proxy?
I believe you're looking for something like this:
import feign.Feign;
import okhttp3.OkHttpClient;
import java.net.InetSocketAddress;
import java.net.Proxy;
...
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy-url", 1234));
OkHttpClient okHttpClient = new OkHttpClient.Builder().proxy(proxy).build();
Feign.builder()
.client(new feign.okhttp.OkHttpClient(okHttpClient))
.target(...);
You just have to additionally add compile 'io.github.openfeign:feign-okhttp:9.5.0' to your project.
The target clause contains your defined Interface. Further reference: https://github.com/OpenFeign/feign
Turns out there is actually a much easier solution.
The following information will be helpful (also for more advanced use cases):
Spring Cloud Commons HTTP Factories
Overriding Feign Defaults
OpenFeign Client can run with several HTTP Clients.
By default it uses java.net.URLConnection, but you can also use ApacheHttpClient or OkHttpClient.
Using Apache Http Client
Here is what you can do to set a proxy using ApacheHttpClient:
Add the following two dependencies to your pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Dependency to switch HttpClient implementation from java.net.URLConnection to Apache HTTP Client -->
<!-- See also: FeignAutoConfiguration for details. -->
<!-- See also: https://cloud.spring.io/spring-cloud-commons/reference/html/#http-clients -->
<!-- See also: https://cloud.spring.io/spring-cloud-openfeign/reference/html/#spring-cloud-feign-overriding-defaults -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
In your app expose the following bean:
// see: https://cloud.spring.io/spring-cloud-commons/reference/html/#http-clients
#Bean
public HttpClientBuilder proxiedHttpClient() {
String proxyHost = "client-envoy";
Integer proxyPort = 80
String proxyScheme = "http";
return HttpClientBuilder.create()
.setProxy(new HttpHost(proxyHost, proxyPort, proxyScheme));
}
That's it - nothing else needs to be configured in application.yaml since ApacheHttpClient will be used by default, if it is on the classpath.
Using Ok Http Client
To set a proxy using OkHttpClient you do a similar thing:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
In your application.yml make sure to enable OkHttpClient and disable ApacheHttpClient:
spring:
cloud:
httpclientfactories:
ok:
enabled: true
apache:
enabled: false
feign:
okhttp:
enabled: true
httpclient:
enabled: false
Instead of HttpClientBuilder expose a bean of type OkHttpClient.Builder.
Spring cloud feign supports three underlying implementations:
Default
Apache HttpClient
OkHttpClient
If using Default:
Create this spring bean (say by defining inside class with #Configuration annotation), no changes required in application properties/yml:
#Bean
public Client feignClient() {
return new Client.Proxied(
null, null, new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)));
}
If using Apache HttpClient:
that means you have feign.httpclient.enabled: true in application.yml and below in your pom.xml or build.gradle:
pom.xml
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
build.gradle
implementation 'io.github.openfeign:feign-httpclient'
Create this spring bean (say by defining inside class with #Configuration annotation):
#Bean
public CloseableHttpClient feignClient() {
return HttpClientBuilder.create().setProxy(new HttpHost(proxyHost, proxyPort)).build();
}
If using OkHttpClient:
that means you have feign.okhttp.enabled: true in application.yml and below in your pom.xml or build.gradle:
pom.xml
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
build.gradle
implementation 'io.github.openfeign:feign-okhttp'
Create this spring bean (say by defining inside class with #Configuration annotation):
#Bean
public OkHttpClient feignClient() {
return new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)))
.build();
}

Resources