Tutorial Section 2 [2/3]
MODIFY
instrumentation-core/spring/README.md
instrumentation-core/spring/README.md
@@ -6,7 +6,7 @@ This package streamlines the manual instrumentation process of OpenTelemetry for
The [first section](#manual-instrumentation-with-java-sdk) will walk you through span creation and propagation using the OpenTelemetry Java API and [Spring's RestTemplate Http Web Client](https://spring.io/guides/gs/consuming-rest/). This approach will use the "vanilla" OpenTelemetry API to make explicit tracing calls within an application's controller.
-The second section will build on the first. It will walk you through implementing spring-web handler and interceptor interfaces to create traces with minimal changes to existing application code. Using the OpenTelemetry API, this approach involves copy and pasting files and a significant amount of manual configurations.
+The [second section](#manual-instrumentation-using-handlers-and-filters) will build on the first. It will walk you through implementing spring-web handler and filter interfaces to create traces with minimal changes to existing application code. Using the OpenTelemetry API, this approach involves copy and pasting files and a significant amount of manual configurations.
The third section will walk you through the annotations and configurations defined in the opentelemetry-instrumentation-spring package. This section will equip you with new tools to streamline the setup and instrumentation of OpenTelemetry on Spring and Spring Boot applications. With these tools you will be able to setup distributed tracing with little to no changes to existing configurations and easily customize traces with minor additions to application code.
@@ -16,11 +16,11 @@ In this guide we will be using a running example. In section one and two, we wil
## Create two Spring Projects
-Using the [spring project initializer](https://start.spring.io/), we will create two spring projects. Name one project `FirstService` and the other `SecondService`. Make sure to select maven, Spring Boot 2.3, Java, and add the spring-web dependency. After downloading the two projects include the OpenTelemetry dependencies and configuration listed below.
+Using the [spring project initializer](https://start.spring.io/), we will create two spring projects. Name one project `MainService` and the other `TimeService`. In this example `MainService` will be a client of `TimeService` and they will be dealing with time. Make sure to select maven, Spring Boot 2.3, Java, and add the spring-web dependency. After downloading the two projects include the OpenTelemetry dependencies and configuration listed below.
## Setup for Manual Instrumentation
-Add the dependencies below to enable OpenTelemetry in `FirstService` and `SecondService`. The Jaeger and LoggingExporter packages are recommended for exporting traces but are not required. As of May 2020, Jaeger, Zipkin, OTLP, and Logging exporters are supported by opentelemetry-java. Feel free to use whatever exporter you are most comfortable with.
+Add the dependencies below to enable OpenTelemetry in `MainService` and `TimeService`. The Jaeger and LoggingExporter packages are recommended for exporting traces but are not required. As of May 2020, Jaeger, Zipkin, OTLP, and Logging exporters are supported by opentelemetry-java. Feel free to use whatever exporter you are most comfortable with.
### Maven
@@ -114,7 +114,7 @@ import io.opentelemetry.exporters.logging.*;
@Configuration
public class OtelConfig {
- private static tracerName = "fooTracer";
+ private static final tracerName = "fooTracer";
@Bean
public Tracer otelTracer() throws Exception {
final Tracer tracer = OpenTelemetry.getTracer(tracerName);
@@ -144,16 +144,16 @@ OpenTelemetrySdk.getTracerProvider().addSpanProcessor(jaegerProcessor);
### Project Background
-Here we will create rest controllers for `FirstService` and `SecondService`.
-`FirstService` will send a GET request to `SecondService` to retrieve the current time. After this request is resolved, `FirstService` then will append a message to time and return a string to the client.
+Here we will create REST controllers for `MainService` and `TimeService`.
+`MainService` will send a GET request to `TimeService` to retrieve the current time. After this request is resolved, `MainService` then will append a message to time and return a string to the client.
## Manual Instrumentation with Java SDK
-### Add OpenTelemetry to FirstService and SecondService
+### Add OpenTelemetry to MainService and TimeService
-Required dependencies and configurations for FirstService and SecondService projects can be found [here](#setup-for-manual-instrumentation).
+Required dependencies and configurations for MainService and TimeService projects can be found [here](#setup-for-manual-instrumentation).
-### FirstService
+### Instrumentation of MainService
1. Ensure OpenTelemetry dependencies are included
2. Ensure an OpenTelemetry Tracer is configured
@@ -162,16 +162,16 @@ Required dependencies and configurations for FirstService and SecondService proj
```java
@SpringBootApplication
-public class FirstServiceApplication {
+public class MainServiceApplication {
public static void main(String[] args) throws IOException {
- SpringApplication.run(FirstServiceApplication.class, args);
+ SpringApplication.run(MainServiceApplication.class, args);
}
}
```
-4. Create a RestController for FirstService
-5. Create a span to wrap the FirstServiceController
+4. Create a REST controller for MainService
+5. Create a span to wrap MainServiceController.message()
```java
import org.springframework.beans.factory.annotation.Autowired;
@@ -187,37 +187,38 @@ import HttpUtils;
@RestController
@RequestMapping(value = "/message")
-public class FirstServiceController {
+public class MainServiceController {
+ private static int requestCount = 1;
+ private static final String TIME_SERVICE_URL = "http://localhost:8081/time";
+
@Autowired
private Tracer tracer;
@Autowired
- HttpUtils httpUtils;
-
- private static String secondServiceUrl = "http://localhost:8081/time";
+ private HttpUtils httpUtils;
@GetMapping
- public String firstTracedMethod() {
+ public String message() {
Span span = tracer.spanBuilder("message").startSpan();
- span.addEvent("Controller Entered");
- span.setAttribute("what.are.you", "Je suis attribute");
try (Scope scope = tracer.withSpan(span)) {
- return "Second Service says: " + httpUtils.callEndpoint(secondServiceUrl);
+ span.addEvent("Controller Entered");
+ span.setAttribute("timeservicecontroller.request.count", requestCount++);
+ return "Time Service says: " + httpUtils.callEndpoint(TIME_SERVICE_URL);
} catch (Exception e) {
- span.addEvent(e.toString());
span.setAttribute("error", true);
return "ERROR: I can't tell the time";
} finally {
+ span.addEvent("Exit Controller");
span.end();
}
}
}
```
-6. Configure `HttpUtils.callEndpoint` to inject span context into request. This is key to propagate the trace to the SecondService
+6. Configure `HttpUtils.callEndpoint` to inject span context into request. This is key to propagate the trace to the TimeService
-HttpUtils is a helper class that injects the current span context into outgoing requests. This involves adding the tracer id and the trace-state to a request header. For this example, we used `RestTemplate` to send requests from `FirstService` to `SecondService`. A similar approach can be used with popular Java Web Clients such as [okhttp](https://square.github.io/okhttp/) and [apache http client](https://www.tutorialspoint.com/apache_httpclient/apache_httpclient_quick_guide.htm). The key to this implementation is to override the put method in `HttpTextFormat.Setter<?>` to handle your request format. `HttpTextFormat.inject` will use this setter to set `traceparent` and `tracestate` headers in your requests. These values will be used to propagate your span context to external services.
+HttpUtils is a helper class that injects the current span context into outgoing requests. This involves adding the tracer id and the trace-state to a request header. For this example, we used `RestTemplate` to send requests from `MainService` to `TimeService`. A similar approach can be used with popular Java Web Clients such as [okhttp](https://square.github.io/okhttp/) and [apache http client](https://www.tutorialspoint.com/apache_httpclient/apache_httpclient_quick_guide.htm). The key to this implementation is to override the put method in `HttpTextFormat.Setter<?>` to handle your request format. `HttpTextFormat.inject` will use this setter to set `traceparent` and `tracestate` headers in your requests. These values will be used to propagate your span context to external services.
```java
@@ -270,13 +271,12 @@ public class HttpUtils {
}
}
```
-### SecondService
+### Instrumentation of TimeService
1. Ensure OpenTelemetry dependencies are included
2. Ensure an OpenTelemetry Tracer is configured
3. Ensure a Spring Boot main class was created by the Spring initializer
-
```java
import java.io.IOException;
@@ -284,16 +284,16 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
-public class SecondServiceApplication {
+public class TimeServiceApplication {
public static void main(String[] args) throws IOException {
- SpringApplication.run(SecondServiceApplication.class, args);
+ SpringApplication.run(TimeServiceApplication.class, args);
}
}
```
-4. Create a RestController for SecondService
-5. Start a span to wrap the SecondServiceController
+4. Create a REST controller for TimeService
+5. Start a span to wrap TimeServiceController.time()
```java
import org.springframework.beans.factory.annotation.Autowired;
@@ -307,17 +307,17 @@ import io.opentelemetry.trace.Tracer;
@RestController
@RequestMapping(value = "/time")
-public class SecondServiceController {
+public class TimeServiceController {
@Autowired
private Tracer tracer;
@GetMapping
- public String callSecondTracedMethod() {
+ public String time() {
Span span = tracer.spanBuilder("time").startSpan();
- span.addEvent("SecondServiceController Entered");
- span.setAttribute("what.am.i", "Tu es une legume");
- try{
+ try (Scope scope = tracer.withSpan(span)) {
+ span.addEvent("TimeServiceController Entered");
+ span.setAttribute("what.am.i", "Tu es une legume");
return "It's time to get a watch";
} finally {
span.end();
@@ -326,7 +326,7 @@ public class SecondServiceController {
}
```
-### Run FirstService and SecondService
+### Run MainService and TimeService
***To view your distributed traces ensure either LogExporter or Jaeger is configured in the OtelConfig.java file***
@@ -338,8 +338,263 @@ After running Jaeger locally, navigate to the url below. Make sure to refresh th
`http://localhost:16686`
-Run FirstService and SecondService from command line or using an IDE. The end point of interest for FirstService is `http://localhost:8080/message` and `http://localhost:8081/time` for SecondService. Entering `localhost:8080/message` in a browser should call FirstService and then SecondService, creating a trace.
+Run MainService and TimeService from command line or using an IDE. The end point of interest for MainService is `http://localhost:8080/message` and `http://localhost:8081/time` for TimeService. Entering `localhost:8080/message` in a browser should call MainService and then TimeService, creating a trace.
-***Note: The default port for the Apache Tomcat is 8080. On localhost both FirstService and SecondService services will attempt to run on this port raising an error. To avoid this add `server.port=8081` to the resources/application.properties file. Ensure the port specified corresponds to port referenced by FirstServiceController.secondServiceUrl. ***
+***Note: The default port for the Apache Tomcat is 8080. On localhost both MainService and TimeService services will attempt to run on this port raising an error. To avoid this add `server.port=8081` to the resources/application.properties file. Ensure the port specified corresponds to port referenced by MainServiceController.TIME_SERVICE_URL. ***
Congrats, we just created a distributed service with OpenTelemetry!
+
+## Manual Instrumentation using Handlers and Filters
+
+In this section, we will implement the javax Servlet Filter interface to wrap all requests to MainService and TimeService controllers in a span.
+
+We will also use the RestTemplate HTTP client to send requests from MainService to TimeService. To propagate the trace in this request we will also implement the ClientHttpRequestInterceptor interface. This implementation is only required for projects that send outbound requests. In this example it is only required for MainService.
+
+### Set up MainService and TimeService
+
+Using the earlier instructions [create two spring projects](#create-two-spring-projects) and add the required [dependencies and configurations](#setup-for-manual-instrumentation).
+
+### Instrumentation of TimeService
+
+Ensure the main method in TimeServiceApplication is defined. This will be the entry point to the TimeService project. This file should be created by the Spring Boot project initializer.
+
+```java
+import java.io.IOException;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class TimeServiceApplication {
+
+ public static void main(String[] args) throws IOException {
+ SpringApplication.run(TimeServiceApplication.class, args);
+ }
+}
+```
+
+Add the REST controller below to your TimeService project. This controller will return a string when TimeServiceController.time is called:
+
+```java
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping(value = "/time")
+public class TimeServiceController {
+ @Autowired
+ private Tracer tracer;
+
+ @GetMapping
+ public String time() {
+ return "It's time to get a watch";
+ }
+}
+```
+
+#### Create Controller Filter
+
+Add the class below to wrap all requests to the TimeServiceController in a span. This class will call the preHandle method before the REST controller is entered and the postHandle method after a response is created.
+
+The preHandle method starts a span for each request. This implementation is shown below:
+
+```java
+
+@Component
+public class ControllerFilter implements Filter {
+ private static final Logger LOG = Logger.getLogger(ControllerFilter.class.getName());
+
+ @Autowired
+ Tracer tracer;
+
+ private final HttpTextFormat.Getter<HttpServletRequest> GETTER =
+ new HttpTextFormat.Getter<HttpServletRequest>() {
+ public String get(HttpServletRequest req, String key) {
+ return req.getHeader(key);
+ }
+ };
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ LOG.info("start doFilter");
+
+ HttpServletRequest req = (HttpServletRequest) request;
+ Span currentSpan;
+ try (Scope scope = tracer.withSpan(currentSpan)) {
+ Context context = OpenTelemetry.getPropagators().getHttpTextFormat()
+ .extract(Context.current(), req, GETTER);
+ currentSpan = createSpanWithParent(req, context);
+ currentSpan.addEvent("dofilter");
+ chain.doFilter(req, response);
+ } finally {
+ currentSpan.end();
+ }
+
+ LOG.info("end doFilter");
+ }
+
+ private Span createSpanWithParent(HttpServletRequest request, Context context) {
+ return tracer.spanBuilder(request.getRequestURI()).setSpanKind(Span.Kind.SERVER).startSpan();
+ }
+}
+
+```
+
+Now your TimeService application is complete. Create the MainService application using the instructions below and then run your distributed service!
+
+### Instrumentation of MainService
+
+Ensure the main method in MainServiceApplication is defined. This will be the entry point to the MainService project. This file should be created by the Spring Boot project initializer.
+
+```java
+@SpringBootApplication
+public class MainServiceApplication {
+
+ public static void main(String[] args) throws IOException {
+ SpringApplication.run(MainServiceApplication.class, args);
+ }
+}
+```
+
+Create a REST controller for MainService. This controller will send a request to TimeService and then return the response to the client:
+
+```java
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+
+@RestController
+@RequestMapping(value = "/message")
+public class MainServiceController {
+ private static final String TIME_SERVICE_URL = "http://localhost:8081/time";
+
+ @Autowired
+ private Tracer tracer;
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private HttpUtils httpUtils;
+
+ @GetMapping
+ public String message() {
+
+ ResponseEntity<String> response =
+ restTemplate.exchange(TIME_SERVICE_URL, HttpMethod.GET, null, String.class);
+ String currentTime = response.getBody();
+
+ return "Time Service: " + currentTime;
+
+ }
+}
+```
+
+As seen in the setup of TimeService, implement the javax servlet filter interface to wrap requests to the TimeServiceController in a span. In effect, we will be taking a copy of the [ControllerFilter.java](#create-controller-filter) file defined in TimeService and adding it to MainService.
+
+#### Create Client Http Request Interceptor
+
+Next, we will configure the ClientHttpRequestInterceptor to intercept all client HTTP requests made using RestTemplate.
+
+To propagate the span context from MainService to TimeService we must inject the trace parent and trace state into the outgoing request header. In section 1 this was done using the helper class HttpUtils. In this section, we will implement the ClientHttpRequestInterceptor interface and register this interceptor in our application.
+
+Include the two classes below to your MainService project to add this functionality:
+
+
+```java
+
+import java.io.IOException;
+
+import io.grpc.Context;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpRequest;
+
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.stereotype.Component;
+import io.opentelemetry.OpenTelemetry;
+import io.opentelemetry.context.propagation.HttpTextFormat;
+import io.opentelemetry.trace.Span;
+import io.opentelemetry.trace.Tracer;
+
+@Component
+public class RestTemplateInterceptor implements ClientHttpRequestInterceptor {
+
+ @Autowired
+ private Tracer tracer;
+
+ private static final HttpTextFormat.Setter<HttpRequest> setter =
+ new HttpTextFormat.Setter<HttpRequest>() {
+ @Override
+ public void set(HttpRequest carrier, String key, String value) {
+ carrier.getHeaders().set(key, value);
+ }
+ };
+
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body,
+ ClientHttpRequestExecution execution) throws IOException {
+
+ String spanName = request.getMethodValue() + " " + request.getURI().toString();
+ Span currentSpan = tracer.spanBuilder(spanName).setSpanKind(Span.Kind.CLIENT).startSpan();
+
+ try (Scope scope = tracer.withSpan(currentSpan)) {
+ OpenTelemetry.getPropagators().getHttpTextFormat().inject(Context.current(), request, setter);
+ ClientHttpResponse response = execution.execute(request, body);
+ LOG.info(String.format("Request sent from RestTemplateInterceptor"));
+
+ return response;
+ }finally {
+ currentSpan.end();
+ }
+ }
+}
+
+```
+
+```java
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestClientConfig {
+
+ @Autowired
+ RestTemplateHeaderModifierInterceptor restTemplateHeaderModifierInterceptor;
+
+ @Bean
+ public RestTemplate restTemplate() {
+ RestTemplate restTemplate = new RestTemplate();
+
+ restTemplate.getInterceptors().add(restTemplateHeaderModifierInterceptor);
+
+ return restTemplate;
+ }
+}
+```
+
+### Create a distributed trace
+
+By default Spring Boot runs a Tomcat server on port 8080. This tutorial assumes MainService runs on the default port (8080) and TimeService runs on port 8081. This is because we hard coded the TimeService end point in MainServiceController.TIME_SERVICE_URL. To run TimeServiceApplication on port 8081 include `server.port=8081` in the resources/application.properties file.
+
+Run both the MainService and TimeService projects in terminal or using an IDE (ex. Eclipse). The end point for MainService should be `http://localhost:8080/message` and `http://localhost:8081/time` for TimeService. Type both urls in a browser and ensure you receive a 200 response.
+
+To visualize this trace add a trace exporter to one or both of your applications. Instructions on how to setup LogExporter and Jaeger can be seen [above](#tracer-configuration).
+
+To create a sample trace enter `localhost:8080/message` in a browser. This trace should include a span for MainService and a span for TimeService.

RENAME
instrumentation-core/spring/build.gradle
instrumentation-core/spring/spring.gradle
@@ -4,6 +4,10 @@ plugins {
id 'java'
}
+ext {
+ minJavaVersionForTests = JavaVersion.VERSION_1_8
+}
+
group = 'io.opentelemetry.instrumentation'
version = '0.0.1-SNAPSHOT'

MODIFY
instrumentation-core/spring/src/main/java/io/opentelemetry/instrumentation/spring/Application.java
instrumentation-core/spring/src/main/java/io/opentelemetry/instrumentation/spring/Application.java
+/*
+ * Copyright The OpenTelemetry 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
+ *
+ * http://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 io.opentelemetry.instrumentation.spring;
import org.springframework.boot.SpringApplication;
@@ -9,5 +24,4 @@ public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
-
}

MODIFY
instrumentation-core/spring/src/test/java/io/opentelemetry/instrumentation/spring/ApplicationTests.java
instrumentation-core/spring/src/test/java/io/opentelemetry/instrumentation/spring/ApplicationTests.java
+/*
+ * Copyright The OpenTelemetry 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
+ *
+ * http://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 io.opentelemetry.instrumentation.spring;
import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
+@EnableAutoConfiguration(
+ exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
class ApplicationTests {
@Test
void contextLoads() {}
-
}

Tutorial Section 2 [2/3] (#548)