Skip to content

Commit

Permalink
OpenFeign support (#1154)
Browse files Browse the repository at this point in the history
* initial OpenFeign support

Implement Logbook interceptor as Feign.Logger

* Move server bootstrap to separate base class

* Cover catch {} blocks with tests

* Add mockito-inline dependency

* Adapt to a new project structure

* Add logbook-openfeign to BOM

* Add missing test cases, make JaCoCo coverage 100%
  • Loading branch information
sanyarnd authored Oct 12, 2021
1 parent fe6f3cb commit f13fdaa
Show file tree
Hide file tree
Showing 14 changed files with 885 additions and 0 deletions.
5 changes: 5 additions & 0 deletions logbook-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@
<artifactId>logbook-okhttp2</artifactId>
<version>2.14.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-openfeign</artifactId>
<version>2.14.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-servlet</artifactId>
Expand Down
51 changes: 51 additions & 0 deletions logbook-openfeign/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.zalando</groupId>
<artifactId>logbook-parent</artifactId>
<version>2.14.0-SNAPSHOT</version>
<relativePath>../logbook-parent/pom.xml</relativePath>
</parent>

<artifactId>logbook-openfeign</artifactId>
<version>2.14.0-SNAPSHOT</version>
<description>OpenFeign implementations for request and response logging</description>
<scm>
<url>https://github.com/zalando/logbook</url>
<connection>scm:git:git@github.com:zalando/logbook.git</connection>
<developerConnection>scm:git:git@github.com:zalando/logbook.git</developerConnection>
</scm>

<properties>
<feign.version>11.6</feign.version>
</properties>

<dependencies>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-core</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>${feign.version}</version>
<scope>provided</scope>
</dependency>

<!-- test dependencies -->
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-test</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.zalando.logbook.openfeign;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

final class ByteStreams {
private ByteStreams() {
}

static byte[] toByteArray(final InputStream in) throws IOException {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
copy(in, out);
return out.toByteArray();
}

static void copy(final InputStream from, final OutputStream to) throws IOException {
final byte[] buf = new byte[4096];
while (true) {
final int r = from.read(buf);
if (r == -1) {
break;
}
to.write(buf, 0, r);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package org.zalando.logbook.openfeign;

import feign.Request;
import feign.Response;
import lombok.AllArgsConstructor;
import lombok.Generated;
import org.apiguardian.api.API;
import org.zalando.logbook.HttpRequest;
import org.zalando.logbook.HttpResponse;
import org.zalando.logbook.Logbook;
import org.zalando.logbook.Logbook.ResponseProcessingStage;

import java.io.IOException;
import java.io.UncheckedIOException;

/**
* Example usage:
* <pre>{@code
* Logbook logbook = ...;
* FeignLogbookLogger interceptor = new FeignLogbookLogger(logbook);
* client = Feign.builder()
* ...
* .logger(interceptor)
* .logLevel(Logger.Level.FULL)
* ...;
* }</pre>
*/
@API(status = API.Status.EXPERIMENTAL)
@AllArgsConstructor
public final class FeignLogbookLogger extends feign.Logger {
private final Logbook logbook;
// Feign is blocking, so there is no context switch between request and response
private final ThreadLocal<ResponseProcessingStage> stage = new ThreadLocal<>();

@Override
@Generated
// HACK: JaCoCo ignores a code with "*Generated*" annotation
// this method is a rudiment (not called anywhere), and shouldn't be covered
protected void log(String configKey, String format, Object... args) {
/* no-op, logging is delegated to logbook */
}

@Override
protected void logRetry(String configKey, Level logLevel) {
/* no-op, logging is delegated to logbook */
}

@Override
protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) {
/* no-op, logging is delegated to logbook */
return ioe;
}

@Override
protected void logRequest(String configKey, Level logLevel, Request request) {
final HttpRequest httpRequest = LocalRequest.create(request);
try {
ResponseProcessingStage processingStage = logbook.process(httpRequest).write();
stage.set(processingStage);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) {
try {
// Logbook will consume body stream, making it impossible to read it again
// read body here and create new response based on byte array instead
byte[] body = ByteStreams.toByteArray(response.body().asInputStream());

final HttpResponse httpResponse = RemoteResponse.create(response, body);
stage.get().process(httpResponse).write();

// create a copy of response to provide consumed body
return Response.builder()
.status(response.status())
.request(response.request())
.reason(response.reason())
.headers(response.headers())
.body(body)
.build();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.zalando.logbook.openfeign;

import org.zalando.logbook.HttpHeaders;

import java.util.*;

class HeaderUtils {
private HeaderUtils() {
}

/**
* Convert Feign headers to Logbook-compatible format
*
* @param feignHeaders original headers
* @return Logbook headers
*/
static HttpHeaders toLogbookHeaders(Map<String, Collection<String>> feignHeaders) {
Map<String, List<String>> convertedHeaders = new HashMap<>();
for (Map.Entry<String, Collection<String>> header : feignHeaders.entrySet()) {
convertedHeaders.put(header.getKey(), new ArrayList<>(header.getValue()));
}
return HttpHeaders.of(convertedHeaders);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.zalando.logbook.openfeign;

import feign.Request;
import lombok.RequiredArgsConstructor;
import org.zalando.logbook.HttpHeaders;
import org.zalando.logbook.HttpRequest;
import org.zalando.logbook.Origin;

import javax.annotation.Nullable;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Optional;

@RequiredArgsConstructor
final class LocalRequest implements HttpRequest {
private final URI uri;
private final Request.HttpMethod httpMethod;
private final HttpHeaders headers;
private final byte[] body;
private final Charset charset;
private boolean withBody = false;

public static LocalRequest create(Request request) {
return new LocalRequest(
URI.create(request.url()),
request.httpMethod(),
HeaderUtils.toLogbookHeaders(request.headers()),
request.body(),
request.charset()
);
}

@Override
public String getRemote() {
return "localhost";
}

@Override
public String getMethod() {
return httpMethod.toString();
}

@Override
public String getScheme() {
return uri.getScheme() == null ? "" : uri.getScheme();
}

@Override
public String getHost() {
return uri.getHost() == null ? "" : uri.getHost();
}

@Override
public Optional<Integer> getPort() {
return Optional.of(uri).map(URI::getPort).filter(p -> p != -1);
}

@Override
public String getPath() {
return uri.getPath() == null ? "" : uri.getPath();
}

@Override
public String getQuery() {
return uri.getQuery() == null ? "" : uri.getQuery();
}

@Override
public HttpRequest withBody() {
withBody = true;
return this;
}

@Override
public HttpRequest withoutBody() {
withBody = false;
return this;
}

@Override
public String getProtocolVersion() {
// feign doesn't support HTTP/2, their own toString looks like this:
// builder.append(httpMethod).append(' ').append(url).append(" HTTP/1.1\n");
return "HTTP/1.1";
}

@Override
public Origin getOrigin() {
return Origin.LOCAL;
}

@Override
public HttpHeaders getHeaders() {
return headers;
}

@Nullable
@Override
public String getContentType() {
return Optional.ofNullable(headers.get("Content-Type"))
.flatMap(ct -> ct.stream().findFirst())
.orElse(null);
}

@Override
public Charset getCharset() {
return charset == null ? StandardCharsets.UTF_8 : charset;
}

@Override
public byte[] getBody() {
return withBody && body != null ? body : new byte[0];
}
}
Loading

0 comments on commit f13fdaa

Please sign in to comment.