Instrumenting Java Applications with OpenTelemetry
Hands-on tutorial showing how to implement OpenTelemetry in a Java application.
- How to use the OpenTelemetry Java agent with a microservice
- How to configure and implement the OpenTelemetry collector
- Differences between black-box and white-box instrumentation
- How to write your own telemetry data with the OpenTelemetry SDK
- How to send generated traces to Grafana Tempo and AWS X-Ray
- How to send generated metrics to Prometheus and Amazon CloudWatch
- How to switch between observability backends without code changes
About | |
---|---|
✅ AWS experience | 200 - Intermediate |
⏱ Time to complete | 45 minutes |
💰 Cost to complete | Free tier eligible |
🧩 Prerequisites | - Docker 4.11+ (Required) - Java 17+ (Required) - Maven 3.8.6+ (Required) - AWS Account (Optional) |
🧑🏻💻 Code | Code used in this tutorial |
build-on-aws-tutorial
that is incomplete. In this tutorial, you will implement the missing part to do things like transmit traces to Grafana Tempo and metrics to Prometheus using OpenTelemetry.- Clone the repository using the tutorial branch:
1
git clone https://github.com/build-on-aws/instrumenting-java-apps-using-opentelemetry -b build-on-aws-tutorial
run-microservice.sh
builds the code and also executes the microservice at the same time.- Change the directory to the folder containing the code.
- Execute the script
run-microservice.sh
.
1
sh run-microservice.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.6.4)
2022-08-24 14:11:19.280 INFO 4523 --- [ main] tutorial.buildon.aws.o11y.HelloApp : Starting HelloApp v1.0 using Java 17.0.2 on redacted-hostname with PID 4523 (/private/tmp/blog/otel-with-java/target/hello-app-1.0.jar started by redacted-user in /private/tmp/blog/otel-with-java)
2022-08-24 14:11:19.284 INFO 4523 --- [ main] tutorial.buildon.aws.o11y.HelloApp : No active profile set, falling back to 1 default profile: "default"
2022-08-24 14:11:21.927 INFO 4523 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8888 (http)
2022-08-24 14:11:21.939 INFO 4523 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-08-24 14:11:21.940 INFO 4523 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.58]
2022-08-24 14:11:22.009 INFO 4523 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-08-24 14:11:22.010 INFO 4523 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2603 ms
2022-08-24 14:11:23.232 INFO 4523 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint(s) beneath base path '/actuator'
2022-08-24 14:11:23.322 INFO 4523 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8888 (http) with context path ''
2022-08-24 14:11:23.344 INFO 4523 --- [ main] tutorial.buildon.aws.o11y.HelloApp : Started HelloApp in 4.905 seconds (JVM running for 5.849)
- Send an HTTP request to the API.
1
curl -X GET http://localhost:8888/hello
1
{"message":"Hello World"}
run-microservice.sh
to shut down the microservice. You can do this by pressing Ctrl+C
. From this point on, every time you need to execute the microservice again with a new version of the code that you changed, just execute the same script.docker-compose.yaml
file with the three services pre-configured.- Start the observability backend using:
1
docker compose up -d
- Open a browser and point the location to http://localhost:3200/status.
- Point your browser to http://localhost:9090/status.
- Point your browser to http://localhost:3000.
Explore
on Grafana. Then, a drop-down in the top will list all the datasources configured, one for Prometheus and another for Grafana Tempo.- Stop the observability backend using:
1
docker compose down
- Edit the file
run-microservice.sh
.
run-microservice.sh
to instruct the script to download the agent for OpenTelemetry that will instrument the microservice during bootstrap. If the agent was downloaded before, meaning that its file is available locally, then the script will simply reuse it. Here is how the updated script should look like.1
2
3
4
5
6
7
8
9
10
mvn clean package -Dmaven.test.skip=true
AGENT_FILE=opentelemetry-javaagent-all.jar
if [ ! -f "${AGENT_FILE}" ]; then
curl -L https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.28.1/aws-opentelemetry-agent.jar --output ${AGENT_FILE}
fi
java -javaagent:./${AGENT_FILE} -jar target/hello-app-1.0.jar
- Execute the script
run-microservice.sh
.
1
[otel.javaagent 2022-08-25 11:18:14:865 -0400] [OkHttp http://localhost:4317/...] ERROR io.opentelemetry.exporter.internal.grpc.OkHttpGrpcExporter - Failed to export spans. The request could not be executed. Full error message: Failed to connect to localhost/[0:0:0:0:0:0:0:1]:4317
- Edit the file
run-microservice.sh
.
run-microservice.sh
file should look:1
2
3
4
5
6
7
8
9
10
11
12
13
mvn clean package -Dmaven.test.skip=true
AGENT_FILE=opentelemetry-javaagent-all.jar
if [ ! -f "${AGENT_FILE}" ]; then
curl -L https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.28.1/aws-opentelemetry-agent.jar --output ${AGENT_FILE}
fi
export OTEL_TRACES_EXPORTER=logging
export OTEL_METRICS_EXPORTER=logging
java -javaagent:./${AGENT_FILE} -jar target/hello-app-1.0.jar
- Execute the script
run-microservice.sh
. - Send an HTTP request to the API.
1
curl -X GET http://localhost:8888/hello
1
2
3
2022-08-25 11:36:27.883 INFO 53938 --- [nio-8888-exec-1] t.buildon.aws.o11y.HelloAppController : The response is valid.
[otel.javaagent 2022-08-25 11:36:28:027 -0400] [http-nio-8888-exec-1] INFO io.opentelemetry.exporter.logging.LoggingSpanExporter - 'HelloAppController.hello' : 630796fbad88856390e52935c0eac4bb 3110a5a25d9c2c3c INTERNAL [tracer: io.opentelemetry.spring-webmvc-3.1:1.16.0-alpha] AttributesMap{data={thread.name=http-nio-8888-exec-1, thread.id=24}, capacity=128, totalAddedValues=2}
[otel.javaagent 2022-08-25 11:36:28:053 -0400] [http-nio-8888-exec-1] INFO io.opentelemetry.exporter.logging.LoggingSpanExporter - '/hello' : 630796fbad88856390e52935c0eac4bb 2a530f1005ab2a5e SERVER [tracer: io.opentelemetry.tomcat-7.0:1.16.0-alpha] AttributesMap{data={http.status_code=200, net.peer.ip=127.0.0.1, thread.name=http-nio-8888-exec-1, http.host=localhost:8888, http.target=/hello, http.flavor=1.1, net.transport=ip_tcp, thread.id=24, http.method=GET, http.scheme=http, net.peer.port=65388, http.route=/hello, http.user_agent=curl/7.79.1}, capacity=128, totalAddedValues=13}
1
2
3
4
5
[otel.javaagent 2022-08-25 11:41:18:676 -0400] [PeriodicMetricReader-1] INFO io.opentelemetry.exporter.logging.LoggingMetricExporter - Received a collection of 57 metrics for export.
[otel.javaagent 2022-08-25 11:41:18:677 -0400] [PeriodicMetricReader-1] INFO io.opentelemetry.exporter.logging.LoggingMetricExporter - metric: ImmutableMetricData{resource=Resource{schemaUrl=https://opentelemetry.io/schemas/1.9.0, attributes={host.arch="x86_64", host.name="redacted-hostname", os.description="Mac OS X 12.5.1", os.type="darwin", process.command_line="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java -javaagent:./opentelemetry-javaagent-all.jar", process.executable.path="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java", process.pid=55132, process.runtime.description="Amazon.com Inc. OpenJDK 64-Bit Server VM 17.0.2+8-LTS", process.runtime.name="OpenJDK Runtime Environment", process.runtime.version="17.0.2+8-LTS", service.name="unknown_service:java", telemetry.auto.version="1.16.0-aws", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.16.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=io.opentelemetry.runtime-metrics, version=null, schemaUrl=null}, name=process.runtime.jvm.classes.loaded, description=Number of classes loaded since JVM start, unit=1, type=LONG_SUM, data=ImmutableSumData{points=[ImmutableLongPointData{startEpochNanos=1661442018630466000, epochNanos=1661442078643186000, attributes={}, value=10629, exemplars=[]}], monotonic=true, aggregationTemporality=CUMULATIVE}}
[otel.javaagent 2022-08-25 11:41:18:678 -0400] [PeriodicMetricReader-1] INFO io.opentelemetry.exporter.logging.LoggingMetricExporter - metric: ImmutableMetricData{resource=Resource{schemaUrl=https://opentelemetry.io/schemas/1.9.0, attributes={host.arch="x86_64", host.name="redacted-hostname", os.description="Mac OS X 12.5.1", os.type="darwin", process.command_line="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java -javaagent:./opentelemetry-javaagent-all.jar", process.executable.path="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java", process.pid=55132, process.runtime.description="Amazon.com Inc. OpenJDK 64-Bit Server VM 17.0.2+8-LTS", process.runtime.name="OpenJDK Runtime Environment", process.runtime.version="17.0.2+8-LTS", service.name="unknown_service:java", telemetry.auto.version="1.16.0-aws", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.16.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=io.opentelemetry.runtime-metrics, version=null, schemaUrl=null}, name=process.runtime.jvm.system.cpu.utilization, description=Recent cpu utilization for the whole system, unit=1, type=DOUBLE_GAUGE, data=ImmutableGaugeData{points=[ImmutableDoublePointData{startEpochNanos=1661442018630466000, epochNanos=1661442078643186000, attributes={}, value=0.0, exemplars=[]}]}}
[otel.javaagent 2022-08-25 11:41:18:679 -0400] [PeriodicMetricReader-1] INFO io.opentelemetry.exporter.logging.LoggingMetricExporter - metric: ImmutableMetricData{resource=Resource{schemaUrl=https://opentelemetry.io/schemas/1.9.0, attributes={host.arch="x86_64", host.name="redacted-hostname", os.description="Mac OS X 12.5.1", os.type="darwin", process.command_line="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java -javaagent:./opentelemetry-javaagent-all.jar", process.executable.path="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java", process.pid=55132, process.runtime.description="Amazon.com Inc. OpenJDK 64-Bit Server VM 17.0.2+8-LTS", process.runtime.name="OpenJDK Runtime Environment", process.runtime.version="17.0.2+8-LTS", service.name="unknown_service:java", telemetry.auto.version="1.16.0-aws", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.16.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=io.opentelemetry.runtime-metrics, version=null, schemaUrl=null}, name=process.runtime.jvm.system.cpu.load_1m, description=Average CPU load of the whole system for the last minute, unit=1, type=DOUBLE_GAUGE, data=ImmutableGaugeData{points=[ImmutableDoublePointData{startEpochNanos=1661442018630466000, epochNanos=1661442078643186000, attributes={}, value=5.2939453125, exemplars=[]}]}}
[otel.javaagent 2022-08-25 11:41:18:679 -0400] [PeriodicMetricReader-1] INFO io.opentelemetry.exporter.logging.LoggingMetricExporter - metric: ImmutableMetricData{resource=Resource{schemaUrl=https://opentelemetry.io/schemas/1.9.0, attributes={host.arch="x86_64", host.name="redacted-hostname", os.description="Mac OS X 12.5.1", os.type="darwin", process.command_line="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java -javaagent:./opentelemetry-javaagent-all.jar", process.executable.path="/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java", process.pid=55132, process.runtime.description="Amazon.com Inc. OpenJDK 64-Bit Server VM 17.0.2+8-LTS", process.runtime.name="OpenJDK Runtime Environment", process.runtime.version="17.0.2+8-LTS", service.name="unknown_service:java", telemetry.auto.version="1.16.0-aws", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.16.0"}}, instrumentationScopeInfo=InstrumentationScopeInfo{name=io.opentelemetry.runtime-metrics, version=null, schemaUrl=null}, name=process.runtime.jvm.cpu.utilization, description=Recent cpu utilization for the process, unit=1, type=DOUBLE_GAUGE, data=ImmutableGaugeData{points=[ImmutableDoublePointData{startEpochNanos=1661442018630466000, epochNanos=1661442078643186000, attributes={}, value=0.0, exemplars=[]}]}}
- Create a file named
collector-config-local.yaml
with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:5555
exporters:
logging:
loglevel: debug
service:
pipelines:
metrics:
receivers: [otlp]
exporters: [logging]
traces:
receivers: [otlp]
exporters: [logging]
- Execute the collector with a container:
1
docker run -v $(pwd)/collector-config-local.yaml:/etc/otelcol/config.yaml -p 5555:5555 otel/opentelemetry-collector:latest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2022-08-25T19:40:25.895Z info service/telemetry.go:102 Setting up own telemetry...
2022-08-25T19:40:25.895Z info service/telemetry.go:137 Serving Prometheus metrics {"address": ":8888", "level": "basic"}
2022-08-25T19:40:25.895Z info components/components.go:30 In development component. May change in the future. {"kind": "exporter", "data_type": "traces", "name": "logging", "stability": "in development"}
2022-08-25T19:40:25.895Z info components/components.go:30 In development component. May change in the future. {"kind": "exporter", "data_type": "metrics", "name": "logging", "stability": "in development"}
2022-08-25T19:40:25.896Z info extensions/extensions.go:42 Starting extensions...
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:74 Starting exporters...
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:78 Exporter is starting... {"kind": "exporter", "data_type": "traces", "name": "logging"}
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:82 Exporter started. {"kind": "exporter", "data_type": "traces", "name": "logging"}
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:78 Exporter is starting... {"kind": "exporter", "data_type": "metrics", "name": "logging"}
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:82 Exporter started. {"kind": "exporter", "data_type": "metrics", "name": "logging"}
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:86 Starting processors...
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:98 Starting receivers...
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:102 Receiver is starting... {"kind": "receiver", "name": "otlp", "pipeline": "traces"}
2022-08-25T19:40:25.896Z info otlpreceiver/otlp.go:70 Starting GRPC server on endpoint 0.0.0.0:5555 {"kind": "receiver", "name": "otlp", "pipeline": "traces"}
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:106 Receiver started. {"kind": "receiver", "name": "otlp", "pipeline": "traces"}
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:102 Receiver is starting... {"kind": "receiver", "name": "otlp", "pipeline": "metrics"}
2022-08-25T19:40:25.896Z info pipelines/pipelines.go:106 Receiver started. {"kind": "receiver", "name": "otlp", "pipeline": "metrics"}
2022-08-25T19:40:25.896Z info service/collector.go:215 Starting otelcol... {"Version": "0.56.0", "NumCPU": 4}
2022-08-25T19:40:25.896Z info service/collector.go:128 Everything is ready. Begin running and processing data.
- Edit the file
run-microservice.sh
.
run-microservice.sh
file should look:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mvn clean package -Dmaven.test.skip=true
AGENT_FILE=opentelemetry-javaagent-all.jar
if [ ! -f "${AGENT_FILE}" ]; then
curl -L https://github.com/aws-observability/aws-otel-java-instrumentation/releases/download/v1.28.1/aws-opentelemetry-agent.jar --output ${AGENT_FILE}
fi
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:5555
export OTEL_RESOURCE_ATTRIBUTES=service.name=hello-app,service.version=1.0
java -javaagent:./${AGENT_FILE} -jar target/hello-app-1.0.jar
- Send an HTTP request to the API.
1
curl -X GET http://localhost:8888/hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
2022-08-25T19:50:23.289Z info TracesExporter {"kind": "exporter", "data_type": "traces", "name": "logging", "#spans": 2}
2022-08-25T19:50:23.289Z info ResourceSpans #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.9.0
Resource labels:
-> host.arch: STRING(x86_64)
-> host.name: STRING(b0de28f1021b.ant.amazon.com)
-> os.description: STRING(Mac OS X 12.5.1)
-> os.type: STRING(darwin)
-> process.command_line: STRING(/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java -javaagent:./opentelemetry-javaagent-all.jar)
-> process.executable.path: STRING(/Library/Java/JavaVirtualMachines/amazon-corretto-17.jdk/Contents/Home:bin:java)
-> process.pid: INT(28820)
-> process.runtime.description: STRING(Amazon.com Inc. OpenJDK 64-Bit Server VM 17.0.2+8-LTS)
-> process.runtime.name: STRING(OpenJDK Runtime Environment)
-> process.runtime.version: STRING(17.0.2+8-LTS)
-> service.name: STRING(hello-app)
-> service.version: STRING(1.0)
-> telemetry.auto.version: STRING(1.16.0-aws)
-> telemetry.sdk.language: STRING(java)
-> telemetry.sdk.name: STRING(opentelemetry)
-> telemetry.sdk.version: STRING(1.16.0)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope io.opentelemetry.spring-webmvc-3.1 1.16.0-alpha
Span #0
Trace ID : 6307d27bed1faa3e31f7ff3f1eae4ecf
Parent ID : 27e7a5c4a62d47ba
ID : e8d5205e3f86ff49
Name : HelloAppController.hello
Kind : SPAN_KIND_INTERNAL
Start time : 2022-08-25 19:50:19.89690875 +0000 UTC
End time : 2022-08-25 19:50:20.071752042 +0000 UTC
Status code : STATUS_CODE_UNSET
Status message :
Attributes:
-> thread.name: STRING(http-nio-8888-exec-1)
-> thread.id: INT(25)
ScopeSpans #1
ScopeSpans SchemaURL:
InstrumentationScope io.opentelemetry.tomcat-7.0 1.16.0-alpha
Span #0
Trace ID : 6307d27bed1faa3e31f7ff3f1eae4ecf
Parent ID :
ID : 27e7a5c4a62d47ba
Name : /hello
Kind : SPAN_KIND_SERVER
Start time : 2022-08-25 19:50:19.707696 +0000 UTC
End time : 2022-08-25 19:50:20.099723333 +0000 UTC
Status code : STATUS_CODE_UNSET
Status message :
Attributes:
-> http.status_code: INT(200)
-> net.peer.ip: STRING(127.0.0.1)
-> thread.name: STRING(http-nio-8888-exec-1)
-> http.host: STRING(localhost:8888)
-> http.target: STRING(/hello)
-> http.flavor: STRING(1.1)
-> net.transport: STRING(ip_tcp)
-> thread.id: INT(25)
-> http.method: STRING(GET)
-> http.scheme: STRING(http)
-> net.peer.port: INT(55741)
-> http.route: STRING(/hello)
-> http.user_agent: STRING(curl/7.79.1)
{"kind": "exporter", "data_type": "traces", "name": "logging"}
- Edit the file
collector-config-local.yaml
collector-config-local.yaml
file should look:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:5555
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
logging:
loglevel: debug
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging]
traces:
receivers: [otlp]
processors: [batch]
exporters: [logging]
docker-compose.yaml
that represents the collector.- Edit the file
docker-compose.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
collector:
image: otel/opentelemetry-collector:latest
container_name: collector
hostname: collector
depends_on:
tempo:
condition: service_healthy
prometheus:
condition: service_healthy
command: ["--config=/etc/collector-config.yaml"]
volumes:
- ./collector-config-local.yaml:/etc/collector-config.yaml
ports:
- "5555:5555"
- "6666:6666"
docker-compose.yaml
file. The code is now in good shape so we can switch our focus to the observability backend, and how to configure the collector to send data to it.- Edit the file
collector-config-local.yaml
.
collector-config-local.yaml
file should look:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:5555
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
logging:
loglevel: debug
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging]
traces:
receivers: [otlp]
processors: [batch]
exporters: [logging, otlp]
otlp
that sends telemetry data to the endpoint tempo:4317
. The port 4317 is being used by Grafana Tempo to receive any type of telemetry data; and since TLS is disabled in that port, the option insecure
was used. Then, we have updated the traces pipeline to include another exporter along with the logging. Think about how powerful this is. Instead of just replacing the existing exporter, we added another one. This is useful for scenarios where you want to broadcast the same telemetry data to multiple observability backends — whether you are executing them side-by-side or you are working on a migration project.- Start the observability backend using:
1
docker compose up -d
- Execute the script
run-microservice.sh
.
1
sh run-microservice.sh
- Send a couple HTTP requests to the microservice API like you did in past steps.
- Open your browser and point to the following location: http://localhost:3000.
- Click on the option to
Explore
on Grafana. - In the drop-down at the top, select the option
Tempo
.
- Click on the
Search
tab. - In the
Service Name
drop-down, select the optionhello-app
. - In the
Span Name
drop-down, select the option/hello
. - Click on the blue button named
Run query
in the upper right corner of the UI.
- Click on the
Trace ID
link for one of the traces.
/hello
root span. 4.83ms was spent with code for the HelloAppController
, executed within the context of the microservice API call. In this example, HelloAppController
is a child-span created automatically by the OpenTelemetry agent for Java.- Go to src/main/java folder. You will see the code for the microservice.
- Look into the code of the Java class
HelloAppController.java
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class HelloAppController {
private static final Logger log =
LoggerFactory.getLogger(HelloAppController.class);
public Response hello() {
Response response = buildResponse();
if (response.isValid()) {
log.info("The response is valid.");
}
return response;
}
private Response buildResponse() {
return new Response("Hello World");
}
private record Response (String message) {
private Response {
Objects.requireNonNull(message);
}
private boolean isValid() {
return true;
}
}
}
hello()
which is mapped to the URI /hello
. Anything after this is what actually happens every time you send an HTTP request to the microservice API. Here, you can start having a sense that what Grafana was showing before wasn't complete. For instance, think about where the invocation call to the method buildResponse()
is? What if that method alone took a greater part of the code execution? Working only with the visibility you have right now, you wouldn't know for sure where the time was actually spent. In this example, we know just by looking into the code that the method buildResponse()
won't take much time to execute. But what if that method was executing code against a remote database that may take a while to reply? You would want people looking at Grafana to see that.- Open the file
pom.xml
. - Update the properties section to the following:
1
2
3
4
5
<properties>
<maven.compiler.release>17</maven.compiler.release>
<otel.traces.api.version>0.13.1</otel.traces.api.version>
<otel.metrics.api.version>1.10.0-alpha-rc.1</otel.metrics.api.version>
</properties>
- Still in the
pom.xml
file, add the following dependencies:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-instrumentation-annotations</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api-trace</artifactId>
<version>${otel.traces.api.version}</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api-metrics</artifactId>
<version>${otel.metrics.api.version}</version>
</dependency>
- Save the changes made in the
pom.xml
file.
HelloAppControlller.java
that contains the code of the microservice.- Change its code to the following version:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package tutorial.buildon.aws.o11y;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.annotations.WithSpan;
public class HelloAppController {
private static final Logger log =
LoggerFactory.getLogger(HelloAppController.class);
private String tracesApiVersion;
private final Tracer tracer =
GlobalOpenTelemetry.getTracer("io.opentelemetry.traces.hello",
tracesApiVersion);
public Response hello() {
Response response = buildResponse();
// Creating a custom span
Span span = tracer.spanBuilder("mySpan").startSpan();
try (Scope scope = span.makeCurrent()) {
if (response.isValid()) {
log.info("The response is valid.");
}
} finally {
span.end();
}
return response;
}
private Response buildResponse() {
return new Response("Hello World");
}
private record Response (String message) {
private Response {
Objects.requireNonNull(message);
}
private boolean isValid() {
return true;
}
}
}
tracer
into the class. This is how we are going to programmatically create custom spans for the parts of the code that we want to highlight. Note that, while creating the tracer, we provided the value of the traces API version that we have set in the pom.xml
file. This is useful for debugging purposes, if you feel that certain tracing behaviors are not working as expected, so you can check if that has to do with versioning.hello()
method, we have added a section that creates a span called mySpan
, that will represent the portion of the code that verifies if the response is valid. While creating spans, it is important to always call the method end()
to share how long that span duration was. Here, you control the time it takes. Also, we have added the annotation @WithSpan
to the method buildResponse()
. This means that whenever that method is executed, a span will be created for it, including the correct start and end time of its duration.- Save the changes made to the
HelloAppController.java
file.
- Once the microservice is up and running, send a couple of HTTP request to the microservice API.
- Go to Grafana and search for the newly generated traces.
- Select one of them to visualize its details.
/hello
execution, two child spans were executed. First, the buildResponse()
method, which took 45.67μs to execute. Then, the mySpan
section of the code, which took 1.52ms to execute. More importantly, in this precise order.- Edit the Java class
HelloAppController.java
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package tutorial.buildon.aws.o11y;
import java.util.Objects;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import static tutorial.buildon.aws.o11y.Constants.*;
import static java.lang.Runtime.*;
public class HelloAppController {
private static final Logger log =
LoggerFactory.getLogger(HelloAppController.class);
private String tracesApiVersion;
private String metricsApiVersion;
private final Tracer tracer =
GlobalOpenTelemetry.getTracer("io.opentelemetry.traces.hello",
tracesApiVersion);
private final Meter meter =
GlobalOpenTelemetry.meterBuilder("io.opentelemetry.metrics.hello")
.setInstrumentationVersion(metricsApiVersion)
.build();
private LongCounter numberOfExecutions;
public void createMetrics() {
numberOfExecutions =
meter
.counterBuilder(NUMBER_OF_EXEC_NAME)
.setDescription(NUMBER_OF_EXEC_DESCRIPTION)
.setUnit("int")
.build();
meter
.gaugeBuilder(HEAP_MEMORY_NAME)
.setDescription(HEAP_MEMORY_DESCRIPTION)
.setUnit("byte")
.buildWithCallback(
r -> {
r.record(getRuntime().totalMemory() - getRuntime().freeMemory());
});
}
public Response hello() {
Response response = buildResponse();
// Creating a custom span
Span span = tracer.spanBuilder("mySpan").startSpan();
try (Scope scope = span.makeCurrent()) {
if (response.isValid()) {
log.info("The response is valid.");
}
// Update the synchronous metric
numberOfExecutions.add(1);
} finally {
span.end();
}
return response;
}
private Response buildResponse() {
return new Response("Hello World");
}
private record Response (String message) {
private Response {
Objects.requireNonNull(message);
}
private boolean isValid() {
return true;
}
}
}
meter
into the class. This is how we are going to programmatically create custom metrics for the parts of the code that we want to highlight. Similarly to what we have done with the tracer
, we provided the value of the metrics API version that we have set in the pom.xml
file. We created another field-scoped variable called numberOfExecutions
. This is going to be a monotonic metric that will count every time the microservice is executed, hence the usage of a counter type.createMetrics()
. This method does the job of creating the metrics during bootstrap, hence the usage of the @PostContruct
annotation. Be mindful though, that we create the metrics using distinct approaches. For the metric that counts the number of executions for the microservice, we used the SDK to build a new metric and then assign this metric to the numberOfExecutions
variable. But we create the other metric to monitor the amount of memory the JVM is consuming and don't assign it to any variable. This is because this second metric is an asynchronous metric. We provide a callback method that will be periodically invoked by the OpenTelemetry agent, which will update the value of the metric. This is a non-monotonic metric implemented using a gauge type.numberOfExecutions
variable is synchronous. This means that it is up to you to provide the code that will update the metric. It won't happen automatically. Specifically, we have updated the code from the hello()
method to include the following line:1
numberOfExecutions.add(1);
custom.metric.number.of.exec
and the metric that monitors the amount of memory consumed by the JVM is called custom.metric.heap.memory
.- Save the changes made to the
HelloAppController.java
file.
collector-config-local.yaml
file.- Edit the file
collector-config-local.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:5555
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
logging:
loglevel: debug
prometheus:
endpoint: collector:6666
namespace: default
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [logging, prometheus]
traces:
receivers: [otlp]
processors: [batch]
exporters: [logging, otlp]
prometheus
that will be used to send the metrics to Prometheus. We have also updated the metrics pipeline to include this exporter. This new exporter behaves differently though. Instead of pushing the metrics to Prometheus, it will expose all the metrics in an endpoint using the port 6666. This endpoint will be used by Prometheus to pull the metrics. This process in Prometheus is called scrapping. For this reason, you need to update the Prometheus configuration to gather metrics from this endpoint.- Go to the o11y-backend folder.
- Open the file
prometheus.yaml
.
prometheus.yaml
file should look like.1
2
3
4
5
6
7
8
9
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'collector'
scrape_interval: 5s
static_configs:
- targets: [ 'collector:6666' ]
- Restart the containers
1
docker compose restart
- Send about a dozen HTTP requests to the microservice API.
- Go to Grafana, click on the
Explore
option. - Make sure to select
Prometheus
in the drop-down box. - Click on the button
Metrics browser
.
default
for the namespace. Along with the metrics created automatically by the OpenTelemetry agent for Java, there is the metrics default_custom_metric_heap_memory
and default_custom_metric_number_of_exec
, which were created programmatically in the previous section.- Select the metric
default_custom_metric_number_of_exec
. - Click on the
Use query
button.
default_custom_metric_heap_memory
should provide a better visualization.- Repeat steps
8
through10
but for the metricdefault_custom_metric_heap_memory
.
- Stop all the containers.
1
docker compose down
- If it is currently running, stop the microservice as well.
- Create a file called
collector-config-aws.yaml
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:5555
processors:
batch:
timeout: 5s
send_batch_size: 1024
exporters:
awsemf:
region: 'us-east-1'
log_group_name: '/metrics/otel'
log_stream_name: 'otel-using-java'
awsxray:
region: 'us-east-1'
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [awsemf]
traces:
receivers: [otlp]
processors: [batch]
exporters: [awsxray]
awsemf
exporter to send the metrics to CloudWatch, and the awsxray
to send the traces to X-Ray.- Create a new Docker Compose file called
docker-compose-aws.yaml
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
version: '3.0'
services:
collector:
image: public.ecr.aws/aws-observability/aws-otel-collector:latest
container_name: collector
hostname: collector
command: ["--config=/etc/collector-config.yaml"]
environment:
- AWS_PROFILE=default
volumes:
- ./collector-config-aws.yaml:/etc/collector-config.yaml
- ~/.aws:/root/.aws
ports:
- "5555:5555"
collector
that will use the AWS OpenDistro for OpenTelemetry distribution of the collector. We will provide the file collector-config-aws.yaml
as parameter to define the processing pipeline for this collector. This container image was created to obtain the AWS credentials in different ways, but in this case, you will provide the credentials via the configured credentials stored in the ˜/.aws/credentials
file. Assuming you can have different profiles configured in that file, you are informing which one to use via the environment variable AWS_PROFILE
. You can learn different ways to provide the AWS credentials here.- Start the container for this collector:
1
docker compose -f docker-compose-aws.yaml up -d
- Now execute the microservice by running the script
run-microservice.sh
.
- Open the AWS Management Console.
- Configure the console to point to the region you have implemented in the
collector-config-aws.yaml
file which isus-east-1
.
- Go the X-Ray service by clicking here.
Traces
, right under X-Ray Traces
.- Click the link in the
ID
column to select a specific trace.
- Go to the Amazon CloudWatch service by clicking here.
- Then, click on
All metrics
.
429
metrics collected from your microservice. These are the 427
metrics collected automatically by the OpenTelemetry agent for Java, plus the two custom metrics you implemented.- Look at the metric
custom.metric.number.of.exec
.
0
to 10
that sums the metric for the period of one
hour.- Click in the tab named
Source
and paste the following JSON code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"metrics": [
[ "hello-app", "custom.metric.number.of.exec", "OTelLib", "io.opentelemetry.metrics.hello", { "id": "m1" } ]
],
"sparkline": true,
"view": "gauge",
"stacked": false,
"region": "us-east-1",
"liveData": true,
"yAxis": {
"left": {
"min": 0,
"max": 10
}
},
"stat": "Sum",
"period": 3600,
"setPeriodToTimeRange": false,
"trend": true
}