Skip to content

Commit

Permalink
adds LogbookClientHttpRequestInterceptor for spring RestTemplate (#917)
Browse files Browse the repository at this point in the history
* adds `LogbookClientHttpRequestInterceptor` for spring `RestTemplate` users

* adds documentation for LogbookClientHttpRequestInterceptor

* fixes * imports

* removes dependency on spring annotations

* maintains spring4 compatibility

* creates new module: logbook-spring

* exclude spring-jcl and make spring-web provided scope

* exclude spring-jcl from spring-test

* excludes commons-logging

* removes dependency on httpclient

* spring4 compatibility

* boost coverage

* polish

* new section for spring

* simplify constructors

* move bean definition

* add autoconfiguration docs
  • Loading branch information
nhomble authored Jan 13, 2021
1 parent 9d53d03 commit 215eb87
Show file tree
Hide file tree
Showing 12 changed files with 688 additions and 1 deletion.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,15 @@ OkHttpClient client = new OkHttpClient.Builder()
.build();
```
### Spring
The `logbook-spring` module contains a `ClientHttpRequestInterceptor` to use with `RestTemplate`:
```java
LogbookClientHttpRequestInterceptor interceptor = new LogbookClientHttpRequestInterceptor(logbook);
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(interceptor);
```
### Spring Boot Starter
Logbook comes with a convenient auto configuration for Spring Boot users. It sets up all of the following parts automatically with sensible defaults:
Expand Down Expand Up @@ -679,6 +688,19 @@ or the following table to see a list of possible integration points:
Multiple filters are merged into one.
#### Autoconfigured beans from `logbook-spring`
Some classes from `logbook-spring` are included in the auto configuration.
You can autowire `LogbookClientHttpRequestInterceptor` with code like:
```java
private final RestTemplate restTemplate;
MyClient(RestTemplateBuilder builder, LogbookClientHttpRequestInterceptor interceptor){
this.restTemplate = builder
.additionalInterceptors(interceptor)
.build();
}
```
#### Configuration
The following tables show the available configuration:
Expand Down
4 changes: 4 additions & 0 deletions logbook-spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
<groupId>org.zalando</groupId>
<artifactId>logbook-json</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-spring</artifactId>
</dependency>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-servlet</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import org.zalando.logbook.json.JsonHttpLogFormatter;
import org.zalando.logbook.servlet.LogbookFilter;
import org.zalando.logbook.servlet.SecureLogbookFilter;
import org.zalando.logbook.spring.LogbookClientHttpRequestInterceptor;

import javax.servlet.Filter;
import javax.servlet.Servlet;
Expand All @@ -68,11 +69,11 @@
import static javax.servlet.DispatcherType.REQUEST;
import static org.apiguardian.api.API.Status.INTERNAL;
import static org.apiguardian.api.API.Status.STABLE;
import static org.zalando.logbook.autoconfigure.LogbookAutoConfiguration.ServletFilterConfiguration.newFilter;
import static org.zalando.logbook.BodyFilters.defaultValue;
import static org.zalando.logbook.BodyFilters.truncate;
import static org.zalando.logbook.HeaderFilters.replaceHeaders;
import static org.zalando.logbook.QueryFilters.replaceQuery;
import static org.zalando.logbook.autoconfigure.LogbookAutoConfiguration.ServletFilterConfiguration.newFilter;

@API(status = STABLE)
@Configuration(proxyBeanMethods = false)
Expand Down Expand Up @@ -303,6 +304,12 @@ public HttpLogWriter writer() {
return new DefaultHttpLogWriter();
}

@Bean
@ConditionalOnMissingBean(LogbookClientHttpRequestInterceptor.class)
public LogbookClientHttpRequestInterceptor logbookClientHttpRequestInterceptor(Logbook logbook) {
return new LogbookClientHttpRequestInterceptor(logbook);
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({
HttpClient.class,
Expand Down
72 changes: 72 additions & 0 deletions logbook-spring/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?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.5.0-SNAPSHOT</version>
</parent>

<artifactId>logbook-spring</artifactId>
<version>2.5.0-SNAPSHOT</version>
<description>Spring 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>

<dependencyManagement>
<dependencies>
<!-- Because spring-security would pull in the wrong version otherwise -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-jcl</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.zalando</groupId>
<artifactId>logbook-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-jcl</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.zalando.logbook.spring;

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,116 @@
package org.zalando.logbook.spring;

import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.zalando.logbook.HttpHeaders;
import org.zalando.logbook.HttpRequest;
import org.zalando.logbook.Origin;

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

import static java.nio.charset.StandardCharsets.UTF_8;

@RequiredArgsConstructor
final class LocalRequest implements HttpRequest {

private final org.springframework.http.HttpRequest request;
private final byte[] body;

private boolean withBody = false;

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

@Override
public String getMethod() {
return request.getMethod().name();
}

@Override
public String getScheme() {
return Optional.of(request.getURI())
.map(URI::getScheme)
.orElse("");
}

@Override
public String getHost() {
return Optional.of(request.getURI())
.map(URI::getHost)
.orElse("");
}

@Override
public Optional<Integer> getPort() {
return Optional.of(request.getURI().getPort())
.filter(p -> p != -1);
}

@Override
public String getPath() {
return Optional.of(request.getURI())
.map(URI::getPath)
.orElse("");
}

@Override
public String getQuery() {
return Optional.of(request.getURI())
.map(URI::getQuery)
.orElse("");
}

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

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

@Override
public String getProtocolVersion() {
// TODO find the real thing
return "HTTP/1.1";
}

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

@Override
public HttpHeaders getHeaders() {
return HttpHeaders.of(request.getHeaders());
}

@Nullable
@Override
public String getContentType() {
return Optional
.ofNullable(request.getHeaders().getFirst("Content-Type"))
.orElse(null);
}

@Override
public Charset getCharset() {
return Optional.ofNullable(getContentType())
.map(ct -> MediaType.parseMediaType(ct).getCharset())
.orElse(UTF_8);
}

@Override
public byte[] getBody() throws IOException {
return withBody ? body : new byte[0];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.zalando.logbook.spring;

import lombok.AllArgsConstructor;
import org.apiguardian.api.API;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.zalando.logbook.HttpResponse;
import org.zalando.logbook.Logbook;

import java.io.IOException;

@API(status = API.Status.EXPERIMENTAL)
@AllArgsConstructor
public final class LogbookClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

private final Logbook logbook;

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
final org.zalando.logbook.HttpRequest httpRequest = new LocalRequest(request, body);
final Logbook.ResponseProcessingStage stage = logbook.process(httpRequest).write();

ClientHttpResponse response = execution.execute(request, body);

final HttpResponse httpResponse = new RemoteResponse(response);
stage.process(httpResponse).write();

return response;
}
}
Loading

0 comments on commit 215eb87

Please sign in to comment.