Skip to content

Commit

Permalink
add opentelemetry module
Browse files Browse the repository at this point in the history
  • Loading branch information
jenschude committed Sep 22, 2023
1 parent d274b81 commit 69d0e7c
Show file tree
Hide file tree
Showing 14 changed files with 272 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ cache/
dependencies-without-allowed-license.json
project-licenses-for-check-license-task.json
**/application.properties
opentelemetry-javaagent.jar
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
* It can be registered as TelemetryMiddleware to the {@link io.vrap.rmf.base.client.ClientBuilder#withTelemetryMiddleware(TelemetryMiddleware) ClientBuilder}
* or the ApiRootBuilder.</p>
*
* {@include.example example.ApiRootBuilderTest#addNewRelic()}
* {@include.example example.NewRelicApiRootBuilderTest#addNewRelic()}
*
* <p>The middleware reads the {@link NewRelicContext} from the Request and restores the transaction using a {@link Token}
* The details of the request and response are then reported as {@link Segment} with {@link HttpParameters}</p>
*
* <p>The Context has to be attached in your application either to a {@link io.vrap.rmf.base.client.ContextApiHttpClient}
* or the {@link ApiHttpRequest#addContext(Object) Request} itself.</p>
*
* {@include.example example.ApiRootBuilderTest#contextClient()}
* {@include.example example.NewRelicApiRootBuilderTest#contextClient()}
*
* {@include.example example.ApiRootBuilderTest#contextApiRoot()}
* {@include.example example.NewRelicApiRootBuilderTest#contextApiRoot()}
*
* The {@link NewRelicContextImpl} will {@link Token#expire() expire} the {@link Token} when it's closed. Closing a {@link io.vrap.rmf.base.client.Context}
* is ensured by {@link ContextApiHttpClientImpl#close() ContextApiHttpClient} if it's implementing {@link java.io.Closeable}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import io.vrap.rmf.base.client.ApiHttpClient;
import io.vrap.rmf.base.client.ContextApiHttpClient;

public class ApiRootBuilderTest {
public class NewRelicApiRootBuilderTest {

String projectKey;

Expand Down
32 changes: 32 additions & 0 deletions commercetools/commercetools-monitoring-opentelemetry/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

dependencies {
api project(":rmf:rmf-java-base")
implementation 'io.opentelemetry:opentelemetry-api:1.28.0'

testImplementation project(":commercetools:commercetools-sdk-java-api")
}

sourceSets {
main {
java {
srcDir 'build/generated/src/main/java'
}
}
}

tasks.register('versionTxt') {
doLast {
new File(projectDir, "$buildInfoPath/com/commercetools/monitoring/opentelemetry/").mkdirs()
new File(projectDir, "$buildInfoPath/com/commercetools/monitoring/opentelemetry/BuildInfo.java").text = """
package com.commercetools.monitoring.opentelemetry;
public class BuildInfo {
public static final String VERSION = "$version";
}
"""
}
}

compileJava {
dependsOn versionTxt
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

package com.commercetools.monitoring.opentelemetry;

public class OpenTelemetryInfo {
public static final String CLIENT_DURATION = "client.duration";
public static final String CLIENT_REQUEST_ERROR = "client.request.error";
public static final String CLIENT_REQUEST_TOTAL = "client.request.total";
public static final String HTTP_RESPONSE_STATUS_CODE = "http.response.status_code";
public static final String HTTP_REQUEST_METHOD = "http.request.method";
public static final String JSON_SERIALIZATION = "json.serialization";
public static final String JSON_DESERIALIZATION = "json.deserialization";
public static final String PREFIX = "commercetools";
public static final String REQUEST_BODY_TYPE = "request.body.type";
public static final String RESPONSE_BODY_TYPE = "response.body.type";
public static final String SERVER_ADDRESS = "server.address";
public static final String SERVER_PORT = "server.port";
public static final String UNIT_MS = "ms";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

package com.commercetools.monitoring.opentelemetry;

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.LongHistogram;
import io.opentelemetry.api.metrics.Meter;
import io.vrap.rmf.base.client.ApiHttpRequest;
import io.vrap.rmf.base.client.ApiHttpResponse;
import io.vrap.rmf.base.client.http.TelemetryMiddleware;

/**
* <p>The OpenTelemetry middleware can be used to report metrics about outgoing request to commercetools.
* It can be registered as TelemetryMiddleware to the {@link io.vrap.rmf.base.client.ClientBuilder#withTelemetryMiddleware(TelemetryMiddleware) ClientBuilder}
* or the ApiRootBuilder.</p>
*
* {@include.example example.OpenTelemetryApiRootBuilderTest#addOpenTelemetry()}
*
* <p>The middleware records a histogram about the request timing in milliseconds. The total number of responses and errors is counted.
* The metrics are enriched with the response status code, server address, port and request method.</p>
*/
public class OpenTelemetryMiddleware implements TelemetryMiddleware {
private final LongHistogram histogram;
private final LongCounter errorCounter;
private final LongCounter requestCounter;

public OpenTelemetryMiddleware(final OpenTelemetry otel) {
this(otel, OpenTelemetryInfo.PREFIX);
}

public OpenTelemetryMiddleware(final OpenTelemetry otel, final String prefix) {
Meter meter = otel.meterBuilder(OpenTelemetryResponseSerializer.class.getPackage().getName()).build();
histogram = meter.histogramBuilder(prefix + "." + OpenTelemetryInfo.CLIENT_DURATION)
.ofLongs()
.setUnit(OpenTelemetryInfo.UNIT_MS)
.build();
errorCounter = meter.counterBuilder(prefix + "." + OpenTelemetryInfo.CLIENT_REQUEST_ERROR).build();
requestCounter = meter.counterBuilder(prefix + "." + OpenTelemetryInfo.CLIENT_REQUEST_TOTAL).build();
}

@Override
public CompletableFuture<ApiHttpResponse<byte[]>> invoke(ApiHttpRequest request,
Function<ApiHttpRequest, CompletableFuture<ApiHttpResponse<byte[]>>> next) {
Instant start = Instant.now();
return next.apply(request).thenApply(response -> {
AttributesBuilder builder = Attributes.builder()
.put(OpenTelemetryInfo.HTTP_RESPONSE_STATUS_CODE, response.getStatusCode())
.put(OpenTelemetryInfo.HTTP_REQUEST_METHOD, request.getMethod().name())
.put(OpenTelemetryInfo.SERVER_ADDRESS, request.getUri().getHost());
if (request.getUri().getPort() > 0) {
builder.put(OpenTelemetryInfo.SERVER_PORT, request.getUri().getPort());
}
Attributes attributes = builder.build();
histogram.record(Duration.between(start, Instant.now()).toMillis(), attributes);
requestCounter.add(1, attributes);
if (response.getStatusCode() >= 400) {
errorCounter.add(1, attributes);
}
return response;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@

package com.commercetools.monitoring.opentelemetry;

import java.time.Duration;
import java.time.Instant;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongHistogram;
import io.opentelemetry.api.metrics.Meter;
import io.vrap.rmf.base.client.ApiHttpResponse;
import io.vrap.rmf.base.client.ResponseSerializer;

/**
* <p>The OpenTelemetry ResponseSerializer can be used to report metrics about the De-/Serialization performance.
* It can be registered as a {@link ResponseSerializer} to the {@link io.vrap.rmf.base.client.ClientBuilder#withSerializer(ResponseSerializer)} ClientBuilder
* or the ApiRootBuilder.</p>
*
* {@include.example example.OpenTelemetryApiRootBuilderTest#addSerializer()}
*
* <p>The response serializer records a histogram about the serialization and deserialization timing of JSON in milliseconds.
* The metrics are enriched with the de-/serialized class name.</p>
*/
public class OpenTelemetryResponseSerializer implements ResponseSerializer {
private final ResponseSerializer serializer;

private final LongHistogram serializerHistogram;
private final LongHistogram deserializerHistogram;

public OpenTelemetryResponseSerializer(final ResponseSerializer serializer, final OpenTelemetry otel) {
this(serializer, otel, OpenTelemetryInfo.PREFIX);
}

public OpenTelemetryResponseSerializer(final ResponseSerializer serializer, final OpenTelemetry otel,
final String prefix) {
this.serializer = serializer;
Meter meter = otel.meterBuilder(OpenTelemetryResponseSerializer.class.getPackage().getName()).build();
serializerHistogram = meter.histogramBuilder(prefix + "." + OpenTelemetryInfo.JSON_SERIALIZATION)
.ofLongs()
.setUnit(OpenTelemetryInfo.UNIT_MS)
.build();
deserializerHistogram = meter.histogramBuilder(prefix + "." + OpenTelemetryInfo.JSON_DESERIALIZATION)
.ofLongs()
.setUnit(OpenTelemetryInfo.UNIT_MS)
.build();

}

@Override
public <O> ApiHttpResponse<O> convertResponse(ApiHttpResponse<byte[]> response, Class<O> outputType) {
Instant start = Instant.now();
ApiHttpResponse<O> result = serializer.convertResponse(response, outputType);
Attributes attributes = Attributes.of(AttributeKey.stringKey(OpenTelemetryInfo.RESPONSE_BODY_TYPE),
outputType.getCanonicalName());
deserializerHistogram.record(Duration.between(start, Instant.now()).toMillis(), attributes);
return result;
}

@Override
public <O> ApiHttpResponse<O> convertResponse(ApiHttpResponse<byte[]> response, JavaType outputType) {
Instant start = Instant.now();
ApiHttpResponse<O> result = serializer.convertResponse(response, outputType);
Attributes attributes = Attributes.of(AttributeKey.stringKey(OpenTelemetryInfo.RESPONSE_BODY_TYPE),
outputType.toString());
deserializerHistogram.record(Duration.between(start, Instant.now()).toMillis(), attributes);
return result;
}

@Override
public <O> ApiHttpResponse<O> convertResponse(ApiHttpResponse<byte[]> response, TypeReference<O> outputType) {
Instant start = Instant.now();
ApiHttpResponse<O> result = serializer.convertResponse(response, outputType);
Attributes attributes = Attributes.of(AttributeKey.stringKey(OpenTelemetryInfo.RESPONSE_BODY_TYPE),
outputType.getType().getTypeName());
deserializerHistogram.record(Duration.between(start, Instant.now()).toMillis(), attributes);
return result;
}

@Override
public byte[] toJsonByteArray(Object value) throws JsonProcessingException {
Instant start = Instant.now();
byte[] result = serializer.toJsonByteArray(value);
Attributes attributes = Attributes.of(AttributeKey.stringKey(OpenTelemetryInfo.REQUEST_BODY_TYPE),
value.getClass().getCanonicalName());
serializerHistogram.record(Duration.between(start, Instant.now()).toMillis(), attributes);
return result;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

package com.commercetools.monitoring.opentelemetry;

import io.vrap.rmf.base.client.SolutionInfo;

public class OpenTelemetrySolutionInfo extends SolutionInfo {
public OpenTelemetrySolutionInfo() {
setName("commercetools-monitoring-opentelemetry");
setVersion(BuildInfo.VERSION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.commercetools.monitoring.opentelemetry.OpenTelemetrySolutionInfo
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@

package example;

import com.commercetools.api.defaultconfig.ApiRootBuilder;
import com.commercetools.api.defaultconfig.ServiceRegion;
import com.commercetools.monitoring.opentelemetry.OpenTelemetryMiddleware;
import com.commercetools.monitoring.opentelemetry.OpenTelemetryResponseSerializer;

import io.opentelemetry.api.GlobalOpenTelemetry;
import io.vrap.rmf.base.client.ApiHttpClient;
import io.vrap.rmf.base.client.ResponseSerializer;

public class OpenTelemetryApiRootBuilderTest {

String projectKey;

ApiHttpClient apiHttpClient;

public void addOpenTelemetry() {
ApiHttpClient client = ApiRootBuilder.of()
.defaultClient(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
.withTelemetryMiddleware(new OpenTelemetryMiddleware(GlobalOpenTelemetry.get()))
.buildClient();
}

public void addSerializer() {
ApiHttpClient client = ApiRootBuilder.of()
.defaultClient(ServiceRegion.GCP_EUROPE_WEST1.getApiUrl())
.withSerializer(new OpenTelemetryResponseSerializer(ResponseSerializer.of(), GlobalOpenTelemetry.get()))
.buildClient();
}
}
3 changes: 2 additions & 1 deletion examples/spring-otel/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ ext {
dependencies {
implementation "com.commercetools.sdk:commercetools-sdk-java-api:${versions.commercetools}"
implementation "com.commercetools.sdk:commercetools-apachehttp-client:${versions.commercetools}"

implementation "com.commercetools.sdk:commercetools-monitoring-opentelemetry:${versions.commercetools}"
implementation 'io.opentelemetry:opentelemetry-api:1.28.0'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
public class SpringmvcApplication {

public static void main(String[] args) {
SpringApplication.run(SpringmvcApplication.class, args);
}
SpringApplication.run(SpringmvcApplication.class, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import com.commercetools.api.client.ProjectScopedApiRoot;
import com.commercetools.api.defaultconfig.ApiRootBuilder;

import com.commercetools.monitoring.opentelemetry.OpenTelemetryMiddleware;
import com.commercetools.monitoring.opentelemetry.OpenTelemetryResponseSerializer;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.vrap.rmf.base.client.*;
import io.vrap.rmf.base.client.oauth2.ClientCredentials;

Expand Down Expand Up @@ -35,6 +38,8 @@ private ClientCredentials credentials() {
public ProjectScopedApiRoot apiRoot() {
return ApiRootBuilder.of()
.defaultClient(credentials())
.withSerializer(new OpenTelemetryResponseSerializer(ResponseSerializer.of(), GlobalOpenTelemetry.get()))
.withTelemetryMiddleware(new OpenTelemetryMiddleware(GlobalOpenTelemetry.get()))
.build(projectKey);
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ include 'rmf:okhttp-client'
include 'commercetools:commercetools-http-client'
include 'commercetools:commercetools-javanet-client'
include 'commercetools:commercetools-monitoring-newrelic'
include 'commercetools:commercetools-monitoring-opentelemetry'
include 'commercetools:commercetools-graphql-api'
include 'commercetools:commercetools-sdk-java-api'
include 'commercetools:commercetools-sdk-java-importapi'
Expand Down

0 comments on commit 69d0e7c

Please sign in to comment.