diff --git a/build.gradle b/build.gradle index c7f85124..8d7d8fc9 100644 --- a/build.gradle +++ b/build.gradle @@ -94,6 +94,9 @@ dependencies { // TL signing library implementation group: 'com.truelayer', name: 'truelayer-signing', version: '0.2.6' + // OpenTelemetry + implementation group: 'io.opentelemetry.instrumentation', name: 'opentelemetry-okhttp-3.0', version: '2.11.0-alpha' + // Serialization def jacksonVersion = '2.14.1' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion diff --git a/examples/quarkus-mvc/collector.yaml b/examples/quarkus-mvc/collector.yaml new file mode 100644 index 00000000..034b4f07 --- /dev/null +++ b/examples/quarkus-mvc/collector.yaml @@ -0,0 +1,46 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: otel-collector:4317 +processors: + attributes: + actions: + - key: test.key + value: hello! + action: insert + batch: + memory_limiter: + check_interval: 1s + limit_percentage: 75 + +exporters: + debug: + verbosity: detailed + otlp: + endpoint: jaeger:4317 + tls: + insecure: true + prometheus: + endpoint: otel-collector:3000 + +extensions: + health_check: + zpages: + endpoint: "otel-collector:55679" + +service: + extensions: [health_check, zpages] + pipelines: + traces: + receivers: [otlp] + processors: [batch, attributes, memory_limiter] + exporters: [otlp] + traces/2: + receivers: [ otlp ] + processors: [ batch, attributes, memory_limiter ] + exporters: [ debug ] + metrics: + receivers: [otlp] + processors: [batch, memory_limiter] + exporters: [prometheus] \ No newline at end of file diff --git a/examples/quarkus-mvc/docker-compose.yaml b/examples/quarkus-mvc/docker-compose.yaml new file mode 100644 index 00000000..5fb1adc2 --- /dev/null +++ b/examples/quarkus-mvc/docker-compose.yaml @@ -0,0 +1,34 @@ +name: quarkus-mvc + +services: + +# backend: +# build: . +# depends_on: +# - db +# ports: +# - "8080:8080" + + jaeger: + image: jaegertracing/jaeger:2.1.0 + ports: + - "16686:16686" + + otel-collector: + image: otel/opentelemetry-collector-contrib:0.116.1 + depends_on: + - jaeger + ports: + - "4317:4317" + - "4318:4318" + - "55679:55679" + - "3000:3000" + volumes: + - "./collector.yaml:/etc/otelcol-contrib/config.yaml" + + prometheus: + image: prom/prometheus:v3.0.1 + ports: + - "9090:9090" + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yml \ No newline at end of file diff --git a/examples/quarkus-mvc/justfile b/examples/quarkus-mvc/justfile new file mode 100644 index 00000000..621ddd91 --- /dev/null +++ b/examples/quarkus-mvc/justfile @@ -0,0 +1,4 @@ +alias r:= run + +run: + docker compose up \ No newline at end of file diff --git a/examples/quarkus-mvc/prometheus.yaml b/examples/quarkus-mvc/prometheus.yaml new file mode 100644 index 00000000..f8837ca4 --- /dev/null +++ b/examples/quarkus-mvc/prometheus.yaml @@ -0,0 +1,10 @@ +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + static_configs: + - targets: ['otel-collector:3000'] \ No newline at end of file diff --git a/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/TrueLayerClientProvider.java b/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/TrueLayerClientProvider.java index 072034ed..232fa73c 100644 --- a/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/TrueLayerClientProvider.java +++ b/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/TrueLayerClientProvider.java @@ -1,8 +1,18 @@ package com.truelayer.quarkusmvc; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_VERSION; + import com.truelayer.java.*; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; import jakarta.enterprise.context.ApplicationScoped; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import lombok.SneakyThrows; @@ -21,8 +31,8 @@ public class TrueLayerClientProvider { @ConfigProperty(name = "tl.signing.key_id") String signingKeyId; - @ConfigProperty(name = "tl.signing.private_key_location") - String signingPrivateKeyLocation; + @ConfigProperty(name = "tl.signing.private_key") + String signingPrivateKey; private static final Logger LOG = Logger.getLogger(TrueLayerClientProvider.class); @@ -30,6 +40,7 @@ public class TrueLayerClientProvider { @SneakyThrows public ITrueLayerClient producer() throws IOException { return TrueLayerClient.New() + .withOpenTelemetry(buildOpenTelemetryConfig()) .environment(Environment.sandbox()) .clientCredentials(ClientCredentials.builder() .clientId(clientId) @@ -37,10 +48,30 @@ public ITrueLayerClient producer() throws IOException { .build()) .signingOptions(SigningOptions.builder() .keyId(signingKeyId) - .privateKey(Files.readAllBytes(Path.of(signingPrivateKeyLocation))) + .privateKey(signingPrivateKey.getBytes(StandardCharsets.UTF_8)) .build()) .withHttpLogs(LOG::info) .withCredentialsCaching() .build(); } + + private OpenTelemetry buildOpenTelemetryConfig() { + Resource resource = Resource.getDefault().toBuilder() + .put(SERVICE_NAME, "truelayer-java") + .put(SERVICE_VERSION, "0.1.0") + .build(); + + SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create( + OtlpGrpcSpanExporter.builder().build())) + .setResource(resource) + .build(); + + OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(sdkTracerProvider) + // .setMeterProvider(sdkMeterProvider) + .build(); + + return openTelemetry; + } } diff --git a/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/controllers/DonationsController.java b/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/controllers/DonationsController.java index 8826ccc8..c726daf2 100644 --- a/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/controllers/DonationsController.java +++ b/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/controllers/DonationsController.java @@ -9,9 +9,7 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import java.util.List; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import org.jboss.resteasy.annotations.Form; @Path("/donations") @@ -35,19 +33,6 @@ public TemplateInstance home() { return donations.instance(); } - @GET - @Path("/test") - @SneakyThrows - public Response test() { - var token = trueLayerClient.auth().getOauthToken(List.of("payments")).get(); - - if (token.isError()) { - return Response.serverError().build(); - } - - return Response.ok("token expires in " + token.getData().getExpiresIn()).build(); - } - @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response donate(@Form DonationRequest donationRequest) { diff --git a/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/services/DonationService.java b/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/services/DonationService.java index a3986188..5af0a10f 100644 --- a/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/services/DonationService.java +++ b/examples/quarkus-mvc/src/main/java/com/truelayer/quarkusmvc/services/DonationService.java @@ -2,6 +2,7 @@ import com.truelayer.java.ITrueLayerClient; import com.truelayer.java.entities.CurrencyCode; +import com.truelayer.java.entities.ResourceType; import com.truelayer.java.entities.User; import com.truelayer.java.entities.accountidentifier.SortCodeAccountNumberAccountIdentifier; import com.truelayer.java.payments.entities.CreatePaymentRequest; @@ -63,10 +64,11 @@ public URI createDonationLink(DonationRequest req) { throw new RuntimeException(String.format("create payment error: %s", paymentResponse.getError())); } - return tlClient.hpp() - .getHostedPaymentPageLink( - paymentResponse.getData().getId(), - paymentResponse.getData().getResourceToken(), - URI.create("http://localhost:8080/donations/callback")); + return tlClient.hppLinkBuilder() + .resourceId(paymentResponse.getData().getId()) + .resourceToken(paymentResponse.getData().getResourceToken()) + .resourceType(ResourceType.PAYMENT) + .returnUri(URI.create("http://localhost:8080/donations/callback")) + .build(); } } diff --git a/examples/quarkus-mvc/src/main/resources/application.properties b/examples/quarkus-mvc/src/main/resources/application.properties index dc27bc33..bc58f1cb 100644 --- a/examples/quarkus-mvc/src/main/resources/application.properties +++ b/examples/quarkus-mvc/src/main/resources/application.properties @@ -3,7 +3,7 @@ quarkus.log.level=INFO tl.client.id=to-set tl.client.secret=to-set tl.signing.key_id=to-set -tl.signing.private_key_location=to-set +tl.signing.private_key=to-set quarkus.otel.trace.enabled=true quarkus.otel.metrics.enabled=true diff --git a/examples/quarkus-mvc/src/main/resources/templates/donations.html b/examples/quarkus-mvc/src/main/resources/templates/donations.html index 41aa5641..d93872f4 100644 --- a/examples/quarkus-mvc/src/main/resources/templates/donations.html +++ b/examples/quarkus-mvc/src/main/resources/templates/donations.html @@ -35,14 +35,14 @@

Make a donation

- +
Please choose a name.
- +
Please choose an email.
@@ -53,7 +53,7 @@

Make a donation

GBP
- +
diff --git a/examples/quarkus-mvc/src/native-test/java/com/truelayer/quarkusmvc/GreetingResourceIT.java b/examples/quarkus-mvc/src/native-test/java/com/truelayer/quarkusmvc/GreetingResourceIT.java new file mode 100644 index 00000000..ac09e051 --- /dev/null +++ b/examples/quarkus-mvc/src/native-test/java/com/truelayer/quarkusmvc/GreetingResourceIT.java @@ -0,0 +1,8 @@ +package com.truelayer.quarkusmvc; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class GreetingResourceIT extends GreetingResourceTest { + // Execute the same tests but in packaged mode. +} diff --git a/examples/quarkus-mvc/src/test/java/com/truelayer/quarkusmvc/GreetingResourceTest.java b/examples/quarkus-mvc/src/test/java/com/truelayer/quarkusmvc/GreetingResourceTest.java new file mode 100644 index 00000000..785f8208 --- /dev/null +++ b/examples/quarkus-mvc/src/test/java/com/truelayer/quarkusmvc/GreetingResourceTest.java @@ -0,0 +1,15 @@ +package com.truelayer.quarkusmvc; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +@QuarkusTest +class GreetingResourceTest { + @Test + void testHelloEndpoint() { + given().when().get("/hello").then().statusCode(200).body(is("Hello from Quarkus REST")); + } +} diff --git a/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java b/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java index be53cf96..ca50debd 100644 --- a/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java +++ b/src/main/java/com/truelayer/java/TrueLayerClientBuilder.java @@ -33,6 +33,7 @@ import com.truelayer.java.signupplus.ISignupPlusHandler; import com.truelayer.java.signupplus.SignupPlusHandler; import com.truelayer.java.versioninfo.LibraryInfoLoader; +import io.opentelemetry.api.OpenTelemetry; import java.time.Clock; import java.time.Duration; import java.util.concurrent.ExecutorService; @@ -78,6 +79,8 @@ public class TrueLayerClientBuilder { private ProxyConfiguration proxyConfiguration; + private OpenTelemetry openTelemetry = OpenTelemetry.noop(); + TrueLayerClientBuilder() {} /** @@ -190,7 +193,6 @@ public TrueLayerClientBuilder withCredentialsCaching() { /** * Utility to enable a custom cache for Oauth credentials. - * @param credentialsCache the custom cache implementation * @return the instance of the client builder used */ public TrueLayerClientBuilder withCredentialsCaching(ICredentialsCache credentialsCache) { @@ -208,6 +210,11 @@ public TrueLayerClientBuilder withProxyConfiguration(ProxyConfiguration proxyCon return this; } + public TrueLayerClientBuilder withOpenTelemetry(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + return this; + } + /** * Builds the Java library main class to interact with TrueLayer APIs. * @return a client instance @@ -228,12 +235,13 @@ public TrueLayerClient build() { IAuthenticationHandler authenticationHandler = AuthenticationHandler.New() .clientCredentials(clientCredentials) - .httpClient(RetrofitFactory.build(authServerApiHttpClient, environment.getAuthApiUri())) + .httpClient(RetrofitFactory.build(authServerApiHttpClient, environment.getAuthApiUri(), openTelemetry)) .build(); // We're reusing a client with only User agent and Idempotency key interceptors and give it our base payment // endpoint - ICommonApi commonApi = RetrofitFactory.build(authServerApiHttpClient, environment.getPaymentsApiUri()) + ICommonApi commonApi = RetrofitFactory.build( + authServerApiHttpClient, environment.getPaymentsApiUri(), openTelemetry) .create(ICommonApi.class); ICommonHandler commonHandler = new CommonHandler(commonApi); @@ -241,7 +249,8 @@ public TrueLayerClient build() { // this one represents the baseline for the client used for Signup+ and Payments OkHttpClient authenticatedApiClient = httpClientFactory.buildAuthenticatedApiClient( clientCredentials.clientId, authServerApiHttpClient, authenticationHandler, credentialsCache); - ISignupPlusApi signupPlusApi = RetrofitFactory.build(authenticatedApiClient, environment.getPaymentsApiUri()) + ISignupPlusApi signupPlusApi = RetrofitFactory.build( + authenticatedApiClient, environment.getPaymentsApiUri(), openTelemetry) .create(ISignupPlusApi.class); SignupPlusHandler.SignupPlusHandlerBuilder signupPlusHandlerBuilder = SignupPlusHandler.builder().signupPlusApi(signupPlusApi); @@ -264,7 +273,8 @@ public TrueLayerClient build() { OkHttpClient paymentsHttpClient = httpClientFactory.buildPaymentsApiClient(authenticatedApiClient, signingOptions); - IPaymentsApi paymentsApi = RetrofitFactory.build(paymentsHttpClient, environment.getPaymentsApiUri()) + IPaymentsApi paymentsApi = RetrofitFactory.build( + paymentsHttpClient, environment.getPaymentsApiUri(), openTelemetry) .create(IPaymentsApi.class); PaymentsHandler.PaymentsHandlerBuilder paymentsHandlerBuilder = @@ -275,7 +285,7 @@ public TrueLayerClient build() { IPaymentsHandler paymentsHandler = paymentsHandlerBuilder.build(); IPaymentsProvidersApi paymentsProvidersApi = RetrofitFactory.build( - paymentsHttpClient, environment.getPaymentsApiUri()) + paymentsHttpClient, environment.getPaymentsApiUri(), openTelemetry) .create(IPaymentsProvidersApi.class); PaymentsProvidersHandler.PaymentsProvidersHandlerBuilder paymentsProvidersHandlerBuilder = @@ -286,7 +296,7 @@ public TrueLayerClient build() { IPaymentsProvidersHandler paymentsProvidersHandler = paymentsProvidersHandlerBuilder.build(); IMerchantAccountsApi merchantAccountsApi = RetrofitFactory.build( - paymentsHttpClient, environment.getPaymentsApiUri()) + paymentsHttpClient, environment.getPaymentsApiUri(), openTelemetry) .create(IMerchantAccountsApi.class); MerchantAccountsHandler.MerchantAccountsHandlerBuilder merchantAccountsHandlerBuilder = MerchantAccountsHandler.builder().merchantAccountsApi(merchantAccountsApi); @@ -295,7 +305,8 @@ public TrueLayerClient build() { } IMerchantAccountsHandler merchantAccountsHandler = merchantAccountsHandlerBuilder.build(); - IMandatesApi mandatesApi = RetrofitFactory.build(paymentsHttpClient, environment.getPaymentsApiUri()) + IMandatesApi mandatesApi = RetrofitFactory.build( + paymentsHttpClient, environment.getPaymentsApiUri(), openTelemetry) .create(IMandatesApi.class); MandatesHandler.MandatesHandlerBuilder mandatesHandlerBuilder = MandatesHandler.builder().mandatesApi(mandatesApi); @@ -304,7 +315,8 @@ public TrueLayerClient build() { } IMandatesHandler mandatesHandler = mandatesHandlerBuilder.build(); - IPayoutsApi payoutsApi = RetrofitFactory.build(paymentsHttpClient, environment.getPaymentsApiUri()) + IPayoutsApi payoutsApi = RetrofitFactory.build( + paymentsHttpClient, environment.getPaymentsApiUri(), openTelemetry) .create(IPayoutsApi.class); PayoutsHandler.PayoutsHandlerBuilder payoutsHandlerBuilder = PayoutsHandler.builder().payoutsApi(payoutsApi); diff --git a/src/main/java/com/truelayer/java/http/RetrofitFactory.java b/src/main/java/com/truelayer/java/http/RetrofitFactory.java index 836c7cce..4746cf0b 100644 --- a/src/main/java/com/truelayer/java/http/RetrofitFactory.java +++ b/src/main/java/com/truelayer/java/http/RetrofitFactory.java @@ -1,12 +1,15 @@ package com.truelayer.java.http; import com.truelayer.java.Utils; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.okhttp.v3_0.OkHttpTelemetry; import java.net.URI; import okhttp3.OkHttpClient; import retrofit2.Retrofit; import retrofit2.converter.jackson.JacksonConverterFactory; public class RetrofitFactory { + @Deprecated public static Retrofit build(OkHttpClient httpClient, URI baseUrl) { return new Retrofit.Builder() .client(httpClient) @@ -15,4 +18,13 @@ public static Retrofit build(OkHttpClient httpClient, URI baseUrl) { .addCallAdapterFactory(new TrueLayerApiAdapterFactory()) .build(); } + + public static Retrofit build(OkHttpClient httpClient, URI baseUrl, OpenTelemetry openTelemetry) { + return new Retrofit.Builder() + .callFactory(OkHttpTelemetry.builder(openTelemetry).build().newCallFactory(httpClient)) + .baseUrl(baseUrl.toString()) + .addConverterFactory(JacksonConverterFactory.create(Utils.getObjectMapper())) + .addCallAdapterFactory(new TrueLayerApiAdapterFactory()) + .build(); + } }