diff --git a/pom.xml b/pom.xml index aa04984..a0d09c6 100644 --- a/pom.xml +++ b/pom.xml @@ -8,6 +8,7 @@ quarkus-audit-tools + quarkus-client-logger quarkus-clock-service quarkus-json-service quarkus-message-digest-service diff --git a/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java new file mode 100644 index 0000000..3061c09 --- /dev/null +++ b/quarkus-client-logger/src/main/java/ch/phoenix/oss/quarkus/commons/client/logger/RedactingClientLogger.java @@ -0,0 +1,106 @@ +package ch.phoenix.oss.quarkus.commons.client.logger; + +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import jakarta.enterprise.context.Dependent; +import jakarta.ws.rs.core.HttpHeaders; +import java.util.Map; +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.client.api.ClientLogger; + +/** + * This is based on org.jboss.resteasy.reactive.client.logging.DefaultClientLogger, + * with the only change being that the value of "Authorization" header, when present, + * is redacted. + */ +@Dependent +public class RedactingClientLogger implements ClientLogger { + + private static final Logger log = Logger.getLogger(RedactingClientLogger.class); + + private int bodySize; + + @Override + public void setBodySize(int bodySize) { + this.bodySize = bodySize; + } + + @Override + public void logResponse(HttpClientResponse response, boolean redirect) { + if (!log.isDebugEnabled()) { + return; + } + + //noinspection Convert2Lambda + response.bodyHandler(new Handler<>() { + @Override + public void handle(Buffer body) { + log.debugf( + "%s: %s %s, Status[%d %s], Headers[%s], Body:\n%s", + redirect ? "Redirect" : "Response", + response.request().getMethod(), + response.request().absoluteURI(), + response.statusCode(), + response.statusMessage(), + asString(response.headers()), + bodyToString(body)); + } + }); + } + + @Override + public void logRequest(HttpClientRequest request, Buffer body, boolean omitBody) { + if (!log.isDebugEnabled()) { + return; + } + if (omitBody) { + log.debugf( + "Request: %s %s Headers[%s], Body omitted", + request.getMethod(), request.absoluteURI(), asString(request.headers())); + } else if (body == null || body.length() == 0) { + log.debugf( + "Request: %s %s Headers[%s], Empty body", + request.getMethod(), request.absoluteURI(), asString(request.headers())); + } else { + log.debugf( + "Request: %s %s Headers[%s], Body:\n%s", + request.getMethod(), request.absoluteURI(), asString(request.headers()), bodyToString(body)); + } + } + + private String bodyToString(Buffer body) { + if (body == null) { + return ""; + } else if (bodySize <= 0) { + return body.toString(); + } else { + String bodyAsString = body.toString(); + return bodyAsString.substring(0, Math.min(bodySize, bodyAsString.length())); + } + } + + private String asString(MultiMap headers) { + if (headers.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder((headers.size() * (6 + 1 + 6)) + + (headers.size() - 1)); // this is a very rough estimate of a result like 'key1=value1 key2=value2' + boolean isFirst = true; + for (Map.Entry entry : headers) { + if (isFirst) { + isFirst = false; + } else { + sb.append(' '); + } + + var key = entry.getKey(); + var value = HttpHeaders.AUTHORIZATION.equalsIgnoreCase(key) ? "*****" : entry.getValue(); + + sb.append(key).append('=').append(value); + } + return sb.toString(); + } +}