# Logging and observability If you need visibility into SDK calls, like request/response logging, latency metrics, trace spans, you can wire up a wrapper around `RatepayClient` that adds that behavior without modifying the SDK itself. ## Wrapping the client for logging/metrics A logging/observability wrapper is the same pattern applied to production code instead of tests: wrap a real `RatepayClient` instance, delegate every call to it, and add your cross-cutting behavior around the delegated call. `RatepayClient` declares many operations (authorize, capture, refund, getters, OTP, ...), and a Java class that `implements RatepayClient` must override all of them. The simplest Java approach is a `java.lang.reflect.Proxy`, which logs every call without listing each method. Kotlin can use `by delegate` and override only what it needs. ```java Java import com.ratepay.sdk.*; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Proxy; public final class LoggingRatepayClient { private LoggingRatepayClient() {} public static RatepayClient wrap(RatepayClient delegate) { return (RatepayClient) Proxy.newProxyInstance( RatepayClient.class.getClassLoader(), new Class[] {RatepayClient.class}, (proxy, method, args) -> { long start = System.nanoTime(); try { Object result = method.invoke(delegate, args); log(method.getName(), "OK", start, null); return result; } catch (InvocationTargetException e) { log(method.getName(), "EXCEPTION", start, e.getCause()); throw e.getCause(); } }); } private static void log(String operation, String result, long startNanos, Throwable error) { long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000; if (error != null) { System.err.printf("ratepay.%s failed after %dms: %s%n", operation, elapsedMs, error); } else { System.out.printf("ratepay.%s -> %s (%dms)%n", operation, result, elapsedMs); } } } ``` ```kotlin Kotlin import com.ratepay.sdk.* import com.ratepay.sdk.exception.RatepayException import com.ratepay.sdk.model.api.payment.* class LoggingRatepayClient(private val delegate: RatepayClient) : RatepayClient by delegate { override fun authorize(request: AuthorizationRequest, idempotencyKey: String?): AuthorizeOutcome { val start = System.nanoTime() try { val outcome = delegate.authorize(request, idempotencyKey) log("authorize", outcome.kind().toString(), start) return outcome } catch (e: RatepayException) { log("authorize", "EXCEPTION", start, e) throw e } } private fun log(operation: String, result: String, startNanos: Long, error: Throwable? = null) { val elapsedMs = (System.nanoTime() - startNanos) / 1_000_000 if (error != null) { System.err.println("ratepay.$operation failed after ${elapsedMs}ms: $error") } else { println("ratepay.$operation -> $result (${elapsedMs}ms)") } } } ``` Swap in a real logging framework (SLF4J) or a metrics client (Micrometer) in place of the `System.out`/`System.err` calls shown here. ## Using the wrapper Build the wrapper once, in the same place you build the underlying client, and use the wrapped instance everywhere downstream code would otherwise use `RatepayClient` directly: ```java Java RatepayClient client = LoggingRatepayClient.wrap( RatepayClient.builder() .clientId(System.getenv("RATEPAY_CLIENT_ID")) .clientSecret(System.getenv("RATEPAY_CLIENT_SECRET")) .environment(Environment.PRODUCTION) .build() ); ``` ```kotlin Kotlin val client: RatepayClient = LoggingRatepayClient( RatepayClient { clientId(System.getenv("RATEPAY_CLIENT_ID")) clientSecret(System.getenv("RATEPAY_CLIENT_SECRET")) environment(Environment.PRODUCTION) }, ) ```