Skip to content

Commit

Permalink
Re-implemented servlet integration
Browse files Browse the repository at this point in the history
  • Loading branch information
whiskeysierra committed Feb 22, 2019
1 parent d2f65c1 commit ed3c534
Show file tree
Hide file tree
Showing 17 changed files with 271 additions and 409 deletions.
101 changes: 54 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,15 @@ or create a customized version using the `LogbookBuilder`:
```java
Logbook logbook = Logbook.builder()
.condition(new CustomCondition())
.rawRequestFilter(new CustomRawRequestFilter())
.rawResponseFilter(new CustomRawResponseFilter())
.queryFilter(new CustomQueryFilter())
.headerFilter(new CustomHeaderFilter())
.bodyFilter(new CustomBodyFilter())
.requestFilter(new CustomRequestFilter())
.responseFilter(new CustomResponseFilter())
.formatter(new CustomHttpLogFormatter())
.writer(new CustomHttpLogWriter())
.sink(new DefaultSink(
new CustomHttpLogFormatter(),
new CustomHttpLogWriter()
))
.build();
```

Expand Down Expand Up @@ -180,16 +180,15 @@ Logbook supports different types of filters:
| `ResponseFilter` | `HttpResponse` | response | n/a |

`QueryFilter`, `HeaderFilter` and `BodyFilter` are relatively high-level and should cover all needs in ~90% of all
cases. For more complicated setups one should fallback to the low-level variants, i.e. `RawRequestFilter` and
`RawResponseFilter` as well as `RequestFilter` and `ResponseFilter` respectively (in conjunction with
`ForwardingHttpRequest`/`ForwardingHttpResponse` and `ForwardingHttpRequest`/`ForwardingHttpResponse`).
cases. For more complicated setups one should fallback to the low-level variants, i.e. `RequestFilter` and `ResponseFilter`
respectively (in conjunction with `ForwardingHttpRequest`/`ForwardingHttpResponse`).

You can configure filters like this:

```java
Logbook logbook = Logbook.builder()
.rawRequestFilter(replaceBody(contentType("audio/*"), "mmh mmh mmh mmh"))
.rawResponseFilter(replaceBody(contentType("*/*-stream"), "It just keeps going and going..."))
.requestFilter(replaceBody(contentType("audio/*"), "mmh mmh mmh mmh"))
.responseFilter(replaceBody(contentType("*/*-stream"), "It just keeps going and going..."))
.queryFilter(accessToken())
.queryFilter(replaceQuery("password", "<secret>"))
.headerFilter(authorization())
Expand Down Expand Up @@ -341,9 +340,12 @@ By default, requests and responses are logged with an *slf4j* logger that uses t

```java
Logbook logbook = Logbook.builder()
.writer(new DefaultHttpLogWriter(
LoggerFactory.getLogger("http.wire-log"),
Level.DEBUG))
.sink(new DefaultSink(
new DefaultHttpFormatter(),
new DefaultHttpLogWriter(
LoggerFactory.getLogger("http.wire-log"),
Level.DEBUG)
))
.build();
```

Expand All @@ -353,7 +355,10 @@ An alternative implementation is to log requests and responses to a `PrintStream

```java
Logbook logbook = Logbook.builder()
.writer(new StreamHttpLogWriter(System.err))
.sink(new DefaultSink(
new DefaultHttpFormatter(),
new StreamHttpLogWriter(System.err)
))
.build();
```

Expand All @@ -363,7 +368,10 @@ The `ChunkingHttpLogWriter` will split long messages into smaller chunks and wil

```java
Logbook logbook = Logbook.builder()
.writer(new ChunkingHttpLogWriter(1000, new DefaultHttpLogWriter()))
.sink(new DefaultSink(
new DefaultHttpFormatter(),
new ChunkingHttpLogWriter(1000, new DefaultHttpLogWriter())
))
.build();

```
Expand All @@ -382,15 +390,14 @@ You’ll have to register the `LogbookFilter` as a `Filter` in your filter chain
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
```

or programmatically, via the `ServletContext`:

```java
context.addFilter("LogbookFilter", new LogbookFilter(logbook))
.addMappingForUrlPatterns(EnumSet.of(REQUEST, ASYNC, ERROR), true, "/*");
.addMappingForUrlPatterns(EnumSet.of(REQUEST, ASYNC), true, "/*");
```

The `LogbookFilter` will, by default, treat requests with a `application/x-www-form-urlencoded` body not different from
Expand All @@ -417,12 +424,12 @@ Secure applications usually need a slightly different setup. You should generall
You can easily achieve the former setup by placing the `LogbookFilter` after your security filter. The latter is a little bit more sophisticated. You’ll need two `LogbookFilter` instances — one before your security filter, and one after it:

```java
context.addFilter("unauthorizedLogbookFilter", new LogbookFilter(logbook, Strategy.SECURITY))
.addMappingForUrlPatterns(EnumSet.of(REQUEST, ASYNC, ERROR), true, "/*");
context.addFilter("SecureLogbookFilter", new SecureLogbookFilter(logbook))
.addMappingForUrlPatterns(EnumSet.of(REQUEST, ASYNC), true, "/*");
context.addFilter("securityFilter", new SecurityFilter())
.addMappingForUrlPatterns(EnumSet.of(REQUEST), true, "/*");
context.addFilter("authorizedLogbookFilter", new LogbookFilter(logbook))
.addMappingForUrlPatterns(EnumSet.of(REQUEST, ASYNC, ERROR), true, "/*");
context.addFilter("LogbookFilter", new LogbookFilter(logbook))
.addMappingForUrlPatterns(EnumSet.of(REQUEST, ASYNC), true, "/*");
```

The first logbook filter will log unauthorized requests **only**. The second filter will log authorized requests, as always.
Expand Down Expand Up @@ -511,40 +518,39 @@ Logbook comes with a convenient auto configuration for Spring Boot users. It set
- HTTP-/JSON-style formatter
- Logging writer

| Type | Name | Default |
|-----------------------------|-----------------------------|---------------------------------------------------------------------------|
| `FilterRegistrationBean` | `unauthorizedLogbookFilter` | Based on `LogbookFilter` |
| `FilterRegistrationBean` | `authorizedLogbookFilter` | Based on `LogbookFilter` |
| `Logbook` | | Based on condition, filters, formatter and writer |
| `Predicate<HttpRequest>` | `requestCondition` | No filter; is later combined with `logbook.exclude` and `logbook.exclude` |
| `RawRequestFilter` | | `RawRequestFilters.defaultValue()` |
| `RawResponseFilter` | | `RawResponseFilters.defaultValue()` |
| `HeaderFilter` | | Based on `logbook.obfuscate.headers` |
| `QueryFilter` | | Based on `logbook.obfuscate.parameters` |
| `BodyFilter` | | `BodyFilters.defaultValue()` |
| `RequestFilter` | | `RequestFilter.none()` |
| `ResponseFilter` | | `ResponseFilter.none()` |
| `HttpLogFormatter` | | `JsonHttpLogFormatter` |
| `HttpLogWriter` | | `DefaultHttpLogWriter` |
| Type | Name | Default |
|-----------------------------|-----------------------|---------------------------------------------------------------------------|
| `FilterRegistrationBean` | `secureLogbookFilter` | Based on `LogbookFilter` |
| `FilterRegistrationBean` | `logbookFilter` | Based on `LogbookFilter` |
| `Logbook` | | Based on condition, filters, formatter and writer |
| `Predicate<HttpRequest>` | `requestCondition` | No filter; is later combined with `logbook.exclude` and `logbook.exclude` |
| `HeaderFilter` | | Based on `logbook.obfuscate.headers` |
| `QueryFilter` | | Based on `logbook.obfuscate.parameters` |
| `BodyFilter` | | `BodyFilters.defaultValue()` |
| `RequestFilter` | | `RequestFilter.none()` |
| `ResponseFilter` | | `ResponseFilter.none()` |
| `HttpLogFormatter` | | `JsonHttpLogFormatter` |
| `HttpLogWriter` | | `DefaultHttpLogWriter` |

Multiple filters are merged into one.

#### Configuration

The following tables show the available configuration:

| Configuration | Description | Default |
|--------------------------------|----------------------------------------------------------------------|-------------------------------|
| `logbook.include` | Include only certain URLs (if defined) | `[]` |
| `logbook.exclude` | Exclude certain URLs (overrides `logbook.include`) | `[]` |
| `logbook.filter.enabled` | Enable the [`LogbookFilter(s)`](#servlet) | `true` |
| `logbook.format.style` | [Formatting style](#formatting) (`http`, `json`, `curl` or `splunk`) | `json` |
| `logbook.obfuscate.headers` | List of header names that need obfuscation | `[Authorization]` |
| `logbook.obfuscate.parameters` | List of parameter names that need obfuscation | `[access_token]` |
| `logbook.write.category` | Changes the category of the [`DefaultHttpLogWriter`](#logger) | `org.zalando.logbook.Logbook` |
| `logbook.write.level` | Changes the level of the [`DefaultHttpLogWriter`](#logger) | `TRACE` |
| `logbook.write.chunk-size` | Splits log lines into smaller chunks of size up-to `chunk-size`. | `0` (disabled) |
| `logbook.write.max-body-size` | Truncates the body up to `max-body-size` and appends `...`. | `-1` (disabled) |
| Configuration | Description | Default |
|---------------------------------|----------------------------------------------------------------------|-------------------------------|
| `logbook.include` | Include only certain URLs (if defined) | `[]` |
| `logbook.exclude` | Exclude certain URLs (overrides `logbook.include`) | `[]` |
| `logbook.filter.enabled` | Enable the [`LogbookFilter`](#ser) | `true` |
| `logbook.secure-filter.enabled` | Enable the [`SecureLogbookFilter](#servlet) | `true` |
| `logbook.format.style` | [Formatting style](#formatting) (`http`, `json`, `curl` or `splunk`) | `json` |
| `logbook.obfuscate.headers` | List of header names that need obfuscation | `[Authorization]` |
| `logbook.obfuscate.parameters` | List of parameter names that need obfuscation | `[access_token]` |
| `logbook.write.category` | Changes the category of the [`DefaultHttpLogWriter`](#logger) | `org.zalando.logbook.Logbook` |
| `logbook.write.level` | Changes the level of the [`DefaultHttpLogWriter`](#logger) | `TRACE` |
| `logbook.write.chunk-size` | Splits log lines into smaller chunks of size up-to `chunk-size`. | `0` (disabled) |
| `logbook.write.max-body-size` | Truncates the body up to `max-body-size` and appends `...`. | `-1` (disabled) |

##### Example configuration

Expand All @@ -557,6 +563,7 @@ logbook:
- /actuator/health
- /api/admin/**
filter.enabled: true
secure-filter.enabled: true
format.style: http
obfuscate:
headers:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.zalando.logbook.servlet;

import org.zalando.logbook.HttpRequest;
import org.zalando.logbook.Logbook;
import org.zalando.logbook.Logbook.ResponseProcessingStage;
import org.zalando.logbook.RequestFilter;

import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static org.zalando.logbook.RequestFilters.replaceBody;

abstract class AbstractLogbookFilter implements Filter {

private static final String STAGE = ResponseProcessingStage.class.getName();

private final RequestFilter filter = replaceBody(message -> "<skipped>");
private final Logbook logbook;

protected AbstractLogbookFilter(final Logbook logbook) {
this.logbook = logbook;
}

@Override
public final void init(final FilterConfig filterConfig) {
// no initialization needed by default
}

@Override
public final void doFilter(final ServletRequest request, final ServletResponse response,
final FilterChain chain) throws ServletException, IOException {

if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
throw new IllegalArgumentException(getClass().getSimpleName() + " only supports HTTP");
}

final HttpServletRequest httpRequest = (HttpServletRequest) request;
final HttpServletResponse httpResponse = (HttpServletResponse) response;

doFilter(httpRequest, httpResponse, chain);
}

protected abstract void doFilter(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse,
final FilterChain chain) throws ServletException, IOException;

protected final ResponseProcessingStage logRequest(final HttpServletRequest httpRequest,
final HttpRequest request) throws IOException {

if (isFirstRequest(httpRequest)) {
final ResponseProcessingStage stage = logbook.process(request).write();
httpRequest.setAttribute(STAGE, stage);
return stage;
} else {
return (ResponseProcessingStage) httpRequest.getAttribute(STAGE);
}
}

protected final void logResponse(final RemoteRequest request, final LocalResponse response,
final Logbook.ResponseWritingStage stage) throws IOException {

if (isLastRequest(request)) {
response.getWriter().flush();
stage.write();
}
}

protected final boolean isFirstRequest(final HttpServletRequest request) {
return request.getDispatcherType() != DispatcherType.ASYNC;
}

protected final boolean isLastRequest(final HttpServletRequest request) {
return !request.isAsyncStarted();
}

protected final HttpRequest skipBody(final RemoteRequest request) {
return filter.filter(request);
}

@Override
public final void destroy() {
// no deconstruction needed by default
}

}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,39 @@

import org.apiguardian.api.API;
import org.zalando.logbook.Logbook;
import org.zalando.logbook.Logbook.ResponseWritingStage;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

import static org.apiguardian.api.API.Status.MAINTAINED;
import static org.apiguardian.api.API.Status.STABLE;

@API(status = STABLE)
public final class LogbookFilter implements HttpFilter {

private final Logbook logbook;
private final Strategy strategy;
public final class LogbookFilter extends AbstractLogbookFilter {

public LogbookFilter() {
this(Logbook.create());
}

public LogbookFilter(final Logbook logbook) {
this(logbook, Strategy.NORMAL);
}

@API(status = MAINTAINED)
public LogbookFilter(final Logbook logbook, final Strategy strategy) {
this.logbook = logbook;
this.strategy = strategy;
super(logbook);
}

@Override
public void doFilter(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse,
final FilterChain chain) throws ServletException, IOException {

strategy.doFilter(logbook, httpRequest, httpResponse, chain);
final RemoteRequest request = new RemoteRequest(httpRequest);
final LocalResponse response = new LocalResponse(httpResponse, request.getProtocolVersion());

final ResponseWritingStage stage = logRequest(request, request).process(response);

chain.doFilter(request, response);

logResponse(request, response, stage);
}

}
Loading

0 comments on commit ed3c534

Please sign in to comment.