Repository: zalando/logbook
Pull request #88 (2016-05-13 22:12:22)

Remove guava dependency
Looks like now everybody will do this)


Patch:
diff --git a/logbook-api/pom.xml b/logbook-api/pom.xml
index 3789fa09..888bd182 100644
--- a/logbook-api/pom.xml
+++ b/logbook-api/pom.xml
@@ -24,9 +24,6 @@
         <developerConnection>scm:git:git@github.com:zalando//logbook.git</developerConnection>
     </scm>
 
-    <dependencies>
-    </dependencies>
-
     <distributionManagement>
         <snapshotRepository>
             <id>ossrh</id>
diff --git a/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java b/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java
index ecda3a96..1cbc6f86 100644
--- a/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java
+++ b/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java
@@ -20,13 +20,13 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
-
 import java.nio.charset.Charset;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
 import java.util.TreeMap;
 
-import static com.google.common.collect.Multimaps.newListMultimap;
 import static java.lang.String.CASE_INSENSITIVE_ORDER;
 
 public interface BaseHttpMessage {
@@ -35,22 +35,42 @@ public interface BaseHttpMessage {
 
     Origin getOrigin();
 
-    ListMultimap<String, String> getHeaders();
+    Map<String, List<String>> getHeaders();
 
     String getContentType();
 
     Charset getCharset();
 
-    class Headers {
+    class HeadersBuilder {
+
+        private final Map<String, List<String>> headers;
 
-        Headers() {
+        public HeadersBuilder() {
             // package private so we can trick code coverage
+            headers = new TreeMap<>(CASE_INSENSITIVE_ORDER);
         }
 
-        public static ListMultimap<String, String> create() {
-            return newListMultimap(new TreeMap<>(CASE_INSENSITIVE_ORDER), ArrayList::new);
+        public HeadersBuilder put(final String key, final String value) {
+            final List<String> values = headers.get(key);
+            if (values != null) {
+                values.add(value);
+            } else {
+                final ArrayList<String> list = new ArrayList<>();
+                list.add(value);
+                headers.put(key, list);
+            }
+            return this;
         }
 
-    }
+        public HeadersBuilder put(final String key, final Iterable<String> values) {
+            for (final String value : values) {
+                put(key, value);
+            }
+            return this;
+        }
 
+        public Map<String, List<String>> build() {
+            return Collections.unmodifiableMap(headers);
+        }
+    }
 }
diff --git a/logbook-api/src/main/java/org/zalando/logbook/LogbookFactory.java b/logbook-api/src/main/java/org/zalando/logbook/LogbookFactory.java
index 654d5f67..b768ef69 100644
--- a/logbook-api/src/main/java/org/zalando/logbook/LogbookFactory.java
+++ b/logbook-api/src/main/java/org/zalando/logbook/LogbookFactory.java
@@ -23,12 +23,11 @@ package org.zalando.logbook;
 import javax.annotation.Nullable;
 import java.util.function.Predicate;
 
-import static com.google.common.collect.Iterables.getOnlyElement;
 import static java.util.ServiceLoader.load;
 
 interface LogbookFactory {
 
-    LogbookFactory INSTANCE = getOnlyElement(load(LogbookFactory.class));
+    LogbookFactory INSTANCE = load(LogbookFactory.class).iterator().next();
 
     Logbook create(
             @Nullable final Predicate<RawHttpRequest> condition,
diff --git a/logbook-core/src/main/java/org/zalando/logbook/Conditions.java b/logbook-core/src/main/java/org/zalando/logbook/Conditions.java
index 18be783b..6e0c6384 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/Conditions.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/Conditions.java
@@ -26,6 +26,7 @@ import java.util.function.BiPredicate;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+import static java.util.Collections.emptyList;
 import static org.zalando.logbook.RequestURI.Component.AUTHORITY;
 import static org.zalando.logbook.RequestURI.Component.PATH;
 import static org.zalando.logbook.RequestURI.Component.SCHEME;
@@ -70,7 +71,7 @@ public final class Conditions {
 
     public static Predicate<RawHttpRequest> header(final String key, final String value) {
         return request ->
-                request.getHeaders().containsEntry(key, value);
+                request.getHeaders().getOrDefault(key, emptyList()).contains(value);
     }
 
     public static Predicate<RawHttpRequest> header(final String key, final Predicate<String> predicate) {
@@ -80,8 +81,9 @@ public final class Conditions {
 
     public static Predicate<RawHttpRequest> header(final BiPredicate<String, String> predicate) {
         return request ->
-                request.getHeaders().entries().stream()
-                        .anyMatch(e -> predicate.test(e.getKey(), e.getValue()));
+                request.getHeaders().entrySet().stream()
+                        .anyMatch(e ->
+                                e.getValue().stream().anyMatch(v -> predicate.test(e.getKey(), v)));
     }
 
 }
diff --git a/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java b/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java
index 63b53d58..4b0d430c 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java
@@ -20,9 +20,8 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.Lists;
-
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -59,19 +58,19 @@ public final class DefaultHttpLogFormatter implements HttpLogFormatter {
     private <H extends HttpMessage> String format(final H message, final String type, final String correlationId,
             final Function<H, String> lineCreator)
             throws IOException {
-        final List<String> lines = Lists.newArrayListWithExpectedSize(4);
+        final List<String> lines = new ArrayList<>();
 
         lines.add(direction(message) + " " + type + ": " + correlationId);
         lines.add(lineCreator.apply(message));
         lines.addAll(formatHeaders(message));
 
         final String body = message.getBodyAsString();
-        
+
         if (!body.isEmpty()) {
             lines.add("");
             lines.add(body);
         }
-        
+
         return join(lines);
     }
 
@@ -80,14 +79,14 @@ public final class DefaultHttpLogFormatter implements HttpLogFormatter {
     }
 
     private List<String> formatHeaders(final HttpMessage message) {
-        return message.getHeaders().asMap().entrySet().stream()
+        return message.getHeaders().entrySet().stream()
                 .collect(toMap(Map.Entry::getKey, this::formatHeaderValues))
                 .entrySet().stream()
                 .map(this::formatHeader)
                 .collect(toList());
     }
 
-    private String formatHeaderValues(final Map.Entry<String, Collection<String>> entry) {
+    private String formatHeaderValues(final Map.Entry<String, List<String>> entry) {
         return entry.getValue().stream().collect(joining(", "));
     }
 
diff --git a/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogWriter.java b/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogWriter.java
index be26b6a4..7f888187 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogWriter.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogWriter.java
@@ -20,7 +20,6 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.annotations.VisibleForTesting;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -81,7 +80,6 @@ public final class DefaultHttpLogWriter implements HttpLogWriter {
         }
     }
 
-    @VisibleForTesting
     Logger getLogger() {
         return logger;
     }
diff --git a/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbook.java b/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbook.java
index 9666df5e..454578c4 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbook.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbook.java
@@ -20,8 +20,6 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.annotations.VisibleForTesting;
-
 import java.io.IOException;
 import java.util.Optional;
 import java.util.UUID;
@@ -36,10 +34,10 @@ final class DefaultLogbook implements Logbook {
     private final HttpLogWriter writer;
 
     DefaultLogbook(
-            final Predicate<RawHttpRequest> predicate, 
+            final Predicate<RawHttpRequest> predicate,
             final RequestObfuscator requestObfuscator,
             final ResponseObfuscator responseObfuscator,
-            final HttpLogFormatter formatter, 
+            final HttpLogFormatter formatter,
             final HttpLogWriter writer) {
         this.predicate = predicate;
         this.requestObfuscator = requestObfuscator;
@@ -70,7 +68,6 @@ final class DefaultLogbook implements Logbook {
         }
     }
 
-    @VisibleForTesting
     static class SimplePrecorrelation<I> implements Precorrelation<I> {
 
         private final String id;
@@ -93,7 +90,6 @@ final class DefaultLogbook implements Logbook {
 
     }
 
-    @VisibleForTesting
     static class SimpleCorrelation<I, O> implements Correlation<I, O> {
 
         private final String id;
diff --git a/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbookFactory.java b/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbookFactory.java
index 916e911f..d824ea1e 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbookFactory.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/DefaultLogbookFactory.java
@@ -22,11 +22,9 @@ package org.zalando.logbook;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import java.util.Optional;
 import java.util.function.Predicate;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static org.zalando.logbook.Obfuscators.accessToken;
-import static org.zalando.logbook.Obfuscators.authorization;
 
 public final class DefaultLogbookFactory implements LogbookFactory {
 
@@ -42,15 +40,15 @@ public final class DefaultLogbookFactory implements LogbookFactory {
             @Nullable final HttpLogWriter writer) {
 
 
-        final HeaderObfuscator header = firstNonNull(headerObfuscator, authorization());
-        final BodyObfuscator body = firstNonNull(bodyObfuscator, BodyObfuscator.none());
+        final HeaderObfuscator header = Optional.ofNullable(headerObfuscator).orElseGet(Obfuscators::authorization);
+        final BodyObfuscator body = Optional.ofNullable(bodyObfuscator).orElseGet(BodyObfuscator::none);
 
         return new DefaultLogbook(
-                firstNonNull(condition, $ -> true),
+                Optional.ofNullable(condition).orElse($ -> true),
                 combine(queryObfuscator, header, body, requestObfuscator),
                 combine(header, body, responseObfuscator),
-                firstNonNull(formatter, new DefaultHttpLogFormatter()),
-                firstNonNull(writer, new DefaultHttpLogWriter())
+                Optional.ofNullable(formatter).orElseGet(DefaultHttpLogFormatter::new),
+                Optional.ofNullable(writer).orElseGet(DefaultHttpLogWriter::new)
         );
     }
 
@@ -61,10 +59,10 @@ public final class DefaultLogbookFactory implements LogbookFactory {
             final BodyObfuscator bodyObfuscator,
             @Nullable final RequestObfuscator requestObfuscator) {
 
-        final QueryObfuscator query = firstNonNull(queryObfuscator, accessToken());
+        final QueryObfuscator query = Optional.ofNullable(queryObfuscator).orElseGet(Obfuscators::accessToken);
 
         return RequestObfuscator.merge(
-                firstNonNull(requestObfuscator, RequestObfuscator.none()),
+                Optional.ofNullable(requestObfuscator).orElseGet(RequestObfuscator::none),
                 request -> new ObfuscatedHttpRequest(request, query, headerObfuscator, bodyObfuscator));
     }
 
@@ -75,8 +73,7 @@ public final class DefaultLogbookFactory implements LogbookFactory {
             @Nullable final ResponseObfuscator responseObfuscator) {
 
         return ResponseObfuscator.merge(
-                firstNonNull(responseObfuscator, ResponseObfuscator.none()),
+                Optional.ofNullable(responseObfuscator).orElseGet(ResponseObfuscator::none),
                 response -> new ObfuscatedHttpResponse(response, headerObfuscator, bodyObfuscator));
     }
-
 }
diff --git a/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java b/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java
index abdf65e6..cd36600c 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java
@@ -20,15 +20,13 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ForwardingObject;
-import com.google.common.collect.ListMultimap;
-
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
 
-public abstract class ForwardingHttpMessage extends ForwardingObject implements HttpMessage {
+public abstract class ForwardingHttpMessage implements HttpMessage {
 
-    @Override
     protected abstract HttpMessage delegate();
 
     @Override
@@ -37,7 +35,7 @@ public abstract class ForwardingHttpMessage extends ForwardingObject implements
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return delegate().getHeaders();
     }
 
@@ -66,4 +64,8 @@ public abstract class ForwardingHttpMessage extends ForwardingObject implements
         return delegate().getBodyAsString();
     }
 
+    @Override
+    public String toString() {
+        return delegate().toString();
+    }
 }
diff --git a/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java b/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java
index 89fed77f..16cc917b 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java
@@ -20,19 +20,17 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ForwardingObject;
-import com.google.common.collect.ListMultimap;
-
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
 
-public abstract class ForwardingRawHttpRequest extends ForwardingObject implements RawHttpRequest {
+public abstract class ForwardingRawHttpRequest implements RawHttpRequest {
 
-    @Override
     protected abstract RawHttpRequest delegate();
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return delegate().getHeaders();
     }
 
@@ -101,4 +99,8 @@ public abstract class ForwardingRawHttpRequest extends ForwardingObject implemen
         return delegate().withBody();
     }
 
+    @Override
+    public String toString() {
+        return delegate().toString();
+    }
 }
diff --git a/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java b/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java
index 2194fb83..7573fbd1 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java
@@ -26,23 +26,21 @@ import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonParser;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.base.CharMatcher;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.net.MediaType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.function.Predicate;
+import java.util.regex.Pattern;
 
 public final class JsonHttpLogFormatter implements HttpLogFormatter {
 
-    private static final MediaType APPLICATION_JSON = MediaType.create("application", "json");
-    private static final CharMatcher PRETTY_PRINT = CharMatcher.anyOf("\n\t");
     private static final Logger LOG = LoggerFactory.getLogger(JsonHttpLogFormatter.class);
+    private static final Pattern JSON = Pattern.compile("application/(?:json|[^\t ;]+\\+json)(?:$|\t| |;)");
 
     private final ObjectMapper mapper;
 
@@ -59,20 +57,18 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
         final String correlationId = precorrelation.getId();
         final HttpRequest request = precorrelation.getRequest();
 
-        final ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder();
+        final Map<String, Object> content = new LinkedHashMap<>();
 
-        builder.put("origin", translate(request.getOrigin()));
-        builder.put("type", "request");
-        builder.put("correlation", correlationId);
-        builder.put("protocol", request.getProtocolVersion());
-        builder.put("remote", request.getRemote());
-        builder.put("method", request.getMethod());
-        builder.put("uri", request.getRequestUri());
+        content.put("origin", translate(request.getOrigin()));
+        content.put("type", "request");
+        content.put("correlation", correlationId);
+        content.put("protocol", request.getProtocolVersion());
+        content.put("remote", request.getRemote());
+        content.put("method", request.getMethod());
+        content.put("uri", request.getRequestUri());
 
-        addUnless(builder, "headers", request.getHeaders().asMap(), Map::isEmpty);
-        addBody(request, builder);
-
-        final ImmutableMap<String, Object> content = builder.build();
+        addUnless(content, "headers", request.getHeaders(), Map::isEmpty);
+        addBody(request, content);
 
         return mapper.writeValueAsString(content);
     }
@@ -82,17 +78,15 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
         final String correlationId = correlation.getId();
         final HttpResponse response = correlation.getResponse();
 
-        final ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder();
-
-        builder.put("origin", translate(response.getOrigin()));
-        builder.put("type", "response");
-        builder.put("correlation", correlationId);
-        builder.put("protocol", response.getProtocolVersion());
-        builder.put("status", response.getStatus());
-        addUnless(builder, "headers", response.getHeaders().asMap(), Map::isEmpty);
-        addBody(response, builder);
+        final Map<String, Object> content = new LinkedHashMap<>();
 
-        final ImmutableMap<String, Object> content = builder.build();
+        content.put("origin", translate(response.getOrigin()));
+        content.put("type", "response");
+        content.put("correlation", correlationId);
+        content.put("protocol", response.getProtocolVersion());
+        content.put("status", response.getStatus());
+        addUnless(content, "headers", response.getHeaders(), Map::isEmpty);
+        addBody(response, content);
 
         return mapper.writeValueAsString(content);
     }
@@ -101,7 +95,7 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
         return origin.name().toLowerCase(Locale.ROOT);
     }
 
-    private static <T> void addUnless(final ImmutableMap.Builder<String, Object> target, final String key,
+    private static <T> void addUnless(final Map<String, Object> target, final String key,
                                       final T element, final Predicate<T> predicate) {
 
         if (!predicate.test(element)) {
@@ -109,13 +103,13 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
         }
     }
 
-    private void addBody(final HttpMessage request, final ImmutableMap.Builder<String, Object> builder) throws IOException {
+    private void addBody(final HttpMessage request, final Map<String, Object> map) throws IOException {
         final String body = request.getBodyAsString();
 
         if (isJson(request.getContentType())) {
-            builder.put("body", tryParseBodyAsJson(body));
+            map.put("body", tryParseBodyAsJson(body));
         } else {
-            addUnless(builder, "body", body, String::isEmpty);
+            addUnless(map, "body", body, String::isEmpty);
         }
     }
 
@@ -127,25 +121,15 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
         try {
             return new JsonBody(compactJson(body));
         } catch (final IOException e) {
-            LOG.trace("Unable to parse body as JSON", e);
+            LOG.trace("Unable to parse body as JSON; embedding it as-is", e);
         }
 
         return body;
     }
 
-    private boolean isJson(final String contentType) {
-        if (contentType.isEmpty()) {
-            return false;
-        }
-
-        final MediaType mediaType = MediaType.parse(contentType);
-
-        final boolean isJson = mediaType.is(APPLICATION_JSON);
-
-        final boolean isApplication = mediaType.is(MediaType.ANY_APPLICATION_TYPE);
-        final boolean isCustomJson = mediaType.subtype().endsWith("+json");
+    private boolean isJson(final String type) {
+        return !type.isEmpty() && JSON.matcher(type).lookingAt();
 
-        return isJson || isApplication && isCustomJson;
     }
 
     private String compactJson(final String json) throws IOException {
@@ -174,7 +158,7 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
 
     // this wouldn't catch spaces in json, but that's ok for our use case here
     private boolean isAlreadyCompacted(final String json) {
-        return PRETTY_PRINT.matchesNoneOf(json);
+        return json.indexOf('\n') == -1;
     }
 
     private static final class JsonBody {
diff --git a/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpRequest.java b/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpRequest.java
index e8db2754..066bede4 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpRequest.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpRequest.java
@@ -20,18 +20,16 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
-
 import java.io.IOException;
-
-import static com.google.common.collect.Multimaps.transformEntries;
+import java.util.List;
+import java.util.Map;
 
 public final class ObfuscatedHttpRequest extends ForwardingHttpRequest {
 
     private final HttpRequest request;
     private final QueryObfuscator queryObfuscator;
     private final BodyObfuscator bodyObfuscator;
-    private final ListMultimap<String, String> headers;
+    private final Map<String, List<String>> headers;
 
     ObfuscatedHttpRequest(final HttpRequest request,
             final QueryObfuscator queryObfuscator,
@@ -40,7 +38,7 @@ public final class ObfuscatedHttpRequest extends ForwardingHttpRequest {
         this.request = request;
         this.queryObfuscator = queryObfuscator;
         this.bodyObfuscator = bodyObfuscator;
-        this.headers = transformEntries(request.getHeaders(), headerObfuscator::obfuscate);
+        this.headers = Obfuscators.obfuscateHeaders(request.getHeaders(), headerObfuscator::obfuscate);
     }
 
     @Override
@@ -60,7 +58,7 @@ public final class ObfuscatedHttpRequest extends ForwardingHttpRequest {
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return headers;
     }
 
diff --git a/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpResponse.java b/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpResponse.java
index cdc12e05..b68f6d08 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpResponse.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/ObfuscatedHttpResponse.java
@@ -20,23 +20,21 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
-
 import java.io.IOException;
-
-import static com.google.common.collect.Multimaps.transformEntries;
+import java.util.List;
+import java.util.Map;
 
 public final class ObfuscatedHttpResponse extends ForwardingHttpResponse {
 
     private final HttpResponse response;
     private final BodyObfuscator bodyObfuscator;
-    private final ListMultimap<String, String> headers;
+    private final Map<String, List<String>> headers;
 
     ObfuscatedHttpResponse(final HttpResponse response, final HeaderObfuscator headerObfuscator,
             final BodyObfuscator bodyObfuscator) {
         this.response = response;
         this.bodyObfuscator = bodyObfuscator;
-        this.headers = transformEntries(response.getHeaders(), headerObfuscator::obfuscate);
+        this.headers = Obfuscators.obfuscateHeaders(response.getHeaders(), headerObfuscator::obfuscate);
     }
 
     @Override
@@ -45,7 +43,7 @@ public final class ObfuscatedHttpResponse extends ForwardingHttpResponse {
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return headers;
     }
 
diff --git a/logbook-core/src/main/java/org/zalando/logbook/Obfuscators.java b/logbook-core/src/main/java/org/zalando/logbook/Obfuscators.java
index 2e01f5e1..d4023393 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/Obfuscators.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/Obfuscators.java
@@ -20,11 +20,15 @@ package org.zalando.logbook;
  * #L%
  */
 
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
 
 import static java.util.regex.Pattern.quote;
+import static java.util.stream.Collectors.toList;
 
 public final class Obfuscators {
 
@@ -36,8 +40,7 @@ public final class Obfuscators {
         final Pattern pattern = Pattern.compile("((?:^|&)" + quote(name) + "=)(?:.*?)(&|$)");
         final String replacementPattern = "$1" + replacement + "$2";
 
-        return query ->
-                pattern.matcher(query).replaceAll(replacementPattern);
+        return query -> pattern.matcher(query).replaceAll(replacementPattern);
     }
 
     public static QueryObfuscator accessToken() {
@@ -56,4 +59,12 @@ public final class Obfuscators {
         return obfuscate("Authorization"::equalsIgnoreCase, "XXX");
     }
 
+    static Map<String, List<String>> obfuscateHeaders(final Map<String, List<String>> map, final BiFunction<String, String, String> f) {
+        final BaseHttpMessage.HeadersBuilder builder = new BaseHttpMessage.HeadersBuilder();
+        for (final Map.Entry<String, List<String>> e : map.entrySet()) {
+            final String k = e.getKey();
+            builder.put(k, e.getValue().stream().map(x -> f.apply(k, x)).collect(toList()));
+        }
+        return builder.build();
+    }
 }
diff --git a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java
index 99952058..0098d444 100644
--- a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java
+++ b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java
@@ -20,9 +20,6 @@ package org.zalando.logbook.httpclient;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimaps;
-import com.google.common.io.ByteStreams;
 import org.apache.http.Header;
 import org.apache.http.HttpEntityEnclosingRequest;
 import org.apache.http.HttpRequest;
@@ -32,15 +29,17 @@ import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.entity.ContentType;
 import org.zalando.logbook.Origin;
 import org.zalando.logbook.RawHttpRequest;
+import org.zalando.logbook.io.ByteStreams;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.UnknownHostException;
 import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 final class LocalRequest implements RawHttpRequest, org.zalando.logbook.HttpRequest {
@@ -119,18 +118,18 @@ final class LocalRequest implements RawHttpRequest, org.zalando.logbook.HttpRequ
 
     @Override
     public String getQuery() {
-        return firstNonNull(originalRequestUri.getQuery(), "");
+        return Optional.ofNullable(originalRequestUri.getQuery()).orElse("");
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
-        final ListMultimap<String, String> headers = Headers.create();
+    public Map<String, List<String>> getHeaders() {
+        final HeadersBuilder builder = new HeadersBuilder();
 
         for (final Header header : request.getAllHeaders()) {
-            headers.put(header.getName(), header.getValue());
+            builder.put(header.getName(), header.getValue());
         }
 
-        return Multimaps.unmodifiableListMultimap(headers);
+        return builder.build();
     }
 
     @Override
diff --git a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LogbookHttpRequestInterceptor.java b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LogbookHttpRequestInterceptor.java
index 6c76d433..58404d41 100644
--- a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LogbookHttpRequestInterceptor.java
+++ b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LogbookHttpRequestInterceptor.java
@@ -20,7 +20,6 @@ package org.zalando.logbook.httpclient;
  * #L%
  */
 
-import com.google.common.annotations.VisibleForTesting;
 import org.apache.http.HttpException;
 import org.apache.http.HttpRequest;
 import org.apache.http.HttpRequestInterceptor;
@@ -41,7 +40,6 @@ public final class LogbookHttpRequestInterceptor implements HttpRequestIntercept
         this(logbook, Localhost.resolve());
     }
 
-    @VisibleForTesting
     LogbookHttpRequestInterceptor(final Logbook logbook, final Localhost localhost) {
         this.logbook = logbook;
         this.localhost = localhost;
diff --git a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java
index 4ece252e..7e59a073 100644
--- a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java
+++ b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java
@@ -20,9 +20,6 @@ package org.zalando.logbook.httpclient;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimaps;
-import com.google.common.io.ByteStreams;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
@@ -30,11 +27,14 @@ import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.entity.ContentType;
 import org.zalando.logbook.Origin;
 import org.zalando.logbook.RawHttpResponse;
+import org.zalando.logbook.io.ByteStreams;
 
 import javax.annotation.Nullable;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
 final class RemoteResponse implements RawHttpResponse, org.zalando.logbook.HttpResponse {
@@ -62,14 +62,14 @@ final class RemoteResponse implements RawHttpResponse, org.zalando.logbook.HttpR
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
-        final ListMultimap<String, String> headers = Headers.create();
+    public Map<String, List<String>> getHeaders() {
+        final HeadersBuilder builder = new HeadersBuilder();
 
         for (final Header header : response.getAllHeaders()) {
-            headers.put(header.getName(), header.getValue());
+            builder.put(header.getName(), header.getValue());
         }
 
-        return Multimaps.unmodifiableListMultimap(headers);
+        return builder.build();
     }
 
     @Override
@@ -98,15 +98,15 @@ final class RemoteResponse implements RawHttpResponse, org.zalando.logbook.HttpR
     @Override
     public org.zalando.logbook.HttpResponse withBody() throws IOException {
         @Nullable final HttpEntity entity = response.getEntity();
-        
+
         if (entity == null) {
             this.body = new byte[0];
             return this;
         }
-        
+
         this.body = ByteStreams.toByteArray(entity.getContent());
         response.setEntity(new ByteArrayEntity(body));
-        
+
         return this;
     }
 
diff --git a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/HttpFilter.java b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/HttpFilter.java
index b8bd433a..795ec444 100644
--- a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/HttpFilter.java
+++ b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/HttpFilter.java
@@ -30,34 +30,34 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
-import static com.google.common.base.Preconditions.checkArgument;
 
 interface HttpFilter extends Filter {
 
     @Override
     default void init(final FilterConfig filterConfig) throws ServletException {
-        
+
     }
 
     @Override
     default void doFilter(final ServletRequest request, final ServletResponse response,
             final FilterChain chain) throws ServletException, IOException {
 
-        checkArgument(request instanceof HttpServletRequest, "%s only supports HTTP", getClass().getSimpleName());
-        checkArgument(response instanceof HttpServletResponse, "%s only supports HTTP", getClass().getSimpleName());
+        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);
     }
-    
+
     void doFilter(final HttpServletRequest httpRequest, final HttpServletResponse httpResponse,
                 final FilterChain chain) throws ServletException, IOException;
-    
+
     @Override
     default void destroy() {
-        
+
     }
 
 }
diff --git a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java
index 1de4cb63..b73f2726 100644
--- a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java
+++ b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java
@@ -20,10 +20,6 @@ package org.zalando.logbook.servlet;
  * #L%
  */
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ListMultimap;
-import com.google.common.io.ByteArrayDataOutput;
-import com.google.common.io.ByteStreams;
 import org.zalando.logbook.HttpResponse;
 import org.zalando.logbook.Origin;
 import org.zalando.logbook.RawHttpResponse;
@@ -32,20 +28,22 @@ import javax.annotation.Nullable;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponseWrapper;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.collect.Multimaps.unmodifiableListMultimap;
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
 
+
 final class LocalResponse extends HttpServletResponseWrapper implements RawHttpResponse, HttpResponse {
 
-    private final ByteArrayDataOutput output = ByteStreams.newDataOutput();
+    private final ByteArrayOutputStream output = new ByteArrayOutputStream();
 
     private final TeeServletOutputStream stream;
     private final PrintWriter writer;
@@ -75,19 +73,19 @@ final class LocalResponse extends HttpServletResponseWrapper implements RawHttpR
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
-        final ListMultimap<String, String> headers = Headers.create();
+    public Map<String, List<String>> getHeaders() {
+        final HeadersBuilder builder = new HeadersBuilder();
 
         for (final String header : getHeaderNames()) {
-            headers.putAll(header, getHeaders(header));
+            builder.put(header, getHeaders(header));
         }
 
-        return unmodifiableListMultimap(headers);
+        return builder.build();
     }
 
     @Override
     public String getContentType() {
-        return firstNonNull(super.getContentType(), "");
+        return Optional.ofNullable(super.getContentType()).orElse("");
     }
 
     @Override
@@ -116,8 +114,7 @@ final class LocalResponse extends HttpServletResponseWrapper implements RawHttpR
         return body;
     }
 
-    @VisibleForTesting
-    ByteArrayDataOutput getOutput() {
+    ByteArrayOutputStream getOutput() {
         return output;
     }
 
diff --git a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java
index 66b1e75d..097ea641 100644
--- a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java
+++ b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java
@@ -20,14 +20,10 @@ package org.zalando.logbook.servlet;
  * #L%
  */
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.UnmodifiableIterator;
-import com.google.common.io.ByteArrayDataOutput;
-import com.google.common.io.ByteStreams;
 import org.zalando.logbook.HttpRequest;
 import org.zalando.logbook.Origin;
 import org.zalando.logbook.RawHttpRequest;
+import org.zalando.logbook.io.ByteStreams;
 
 import javax.annotation.Nullable;
 import javax.servlet.ServletInputStream;
@@ -35,21 +31,23 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.nio.charset.Charset;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.collect.Iterators.addAll;
-import static com.google.common.collect.Iterators.forEnumeration;
-import static com.google.common.collect.Multimaps.unmodifiableListMultimap;
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.util.Collections.list;
+
 
 final class RemoteRequest extends HttpServletRequestWrapper implements RawHttpRequest, HttpRequest {
 
-    private final ByteArrayDataOutput output = ByteStreams.newDataOutput();
-    
+    private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+
     /**
      * Null until we successfully intercepted it.
      */
@@ -92,25 +90,25 @@ final class RemoteRequest extends HttpServletRequestWrapper implements RawHttpRe
 
     @Override
     public String getQuery() {
-        return firstNonNull(getQueryString(), "");
+        return Optional.ofNullable(getQueryString()).orElse("");
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
-        final ListMultimap<String, String> headers = Headers.create();
-        final UnmodifiableIterator<String> names = forEnumeration(getHeaderNames());
+    public Map<String, List<String>> getHeaders() {
+        final HeadersBuilder builder = new HeadersBuilder();
+        final Enumeration<String> names = getHeaderNames();
 
-        while (names.hasNext()) {
-            final String name = names.next();
-            addAll(headers.get(name), forEnumeration(getHeaders(name)));
+        while (names.hasMoreElements()) {
+            final String name = names.nextElement();
+            builder.put(name, list(getHeaders(name)));
         }
 
-        return unmodifiableListMultimap(headers);
+        return builder.build();
     }
 
     @Override
     public String getContentType() {
-        return firstNonNull(super.getContentType(), "");
+        return Optional.ofNullable(super.getContentType()).orElse("");
     }
 
     @Override
@@ -145,9 +143,7 @@ final class RemoteRequest extends HttpServletRequestWrapper implements RawHttpRe
         return body;
     }
 
-    @VisibleForTesting
-    ByteArrayDataOutput getOutput() {
+    ByteArrayOutputStream getOutput() {
         return output;
     }
-
 }
diff --git a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java
index 90c54e28..fc6371af 100644
--- a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java
+++ b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/LogbookAutoConfiguration.java
@@ -57,9 +57,9 @@ import org.zalando.logbook.servlet.LogbookFilter;
 
 import javax.servlet.Filter;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Predicate;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
 import static java.util.stream.Collectors.toList;
 import static javax.servlet.DispatcherType.ASYNC;
 import static javax.servlet.DispatcherType.ERROR;
@@ -199,7 +199,7 @@ public class LogbookAutoConfiguration {
     @ConditionalOnMissingBean(name = "httpLogger")
     public Logger httpLogger() {
         final String category = properties.getWrite().getCategory();
-        return LoggerFactory.getLogger(firstNonNull(category, Logbook.class.getName()));
+        return LoggerFactory.getLogger(Optional.ofNullable(category).orElseGet(Logbook.class::getName));
     }
 
 }
diff --git a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/SecurityLogbookAutoConfiguration.java b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/SecurityLogbookAutoConfiguration.java
index a1de9ae2..fce180ed 100644
--- a/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/SecurityLogbookAutoConfiguration.java
+++ b/logbook-spring-boot-starter/src/main/java/org/zalando/logbook/spring/SecurityLogbookAutoConfiguration.java
@@ -2,7 +2,7 @@ package org.zalando.logbook.spring;
 
 /*
  * #%L
- * Logbook: Spring Boot Starter
+ * Logbook: Spring
  * %%
  * Copyright (C) 2015 - 2016 Zalando SE
  * %%
diff --git a/pom.xml b/pom.xml
index fce2f0b6..9933d137 100644
--- a/pom.xml
+++ b/pom.xml
@@ -120,11 +120,6 @@
                 <artifactId>gag</artifactId>
                 <version>1.0.1</version>
             </dependency>
-            <dependency>
-                <groupId>com.google.guava</groupId>
-                <artifactId>guava</artifactId>
-                <version>18.0</version>
-            </dependency>
             <dependency>
                 <groupId>org.projectlombok</groupId>
                 <artifactId>lombok</artifactId>
@@ -324,10 +319,6 @@
             <groupId>com.google.gag</groupId>
             <artifactId>gag</artifactId>
         </dependency>
-        <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>


Test patch:
diff --git a/logbook-api/src/test/java/org/zalando/logbook/BaseHttpMessageTest.java b/logbook-api/src/test/java/org/zalando/logbook/BaseHttpMessageTest.java
index 2bc47427..b67d0f80 100644
--- a/logbook-api/src/test/java/org/zalando/logbook/BaseHttpMessageTest.java
+++ b/logbook-api/src/test/java/org/zalando/logbook/BaseHttpMessageTest.java
@@ -20,9 +20,12 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
 import org.junit.Test;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
 import static org.hamcrest.Matchers.hasItem;
 import static org.junit.Assert.assertThat;
 
@@ -30,9 +33,15 @@ public final class BaseHttpMessageTest {
 
     @Test
     public void shouldUseCaseInsensitiveHeaders() {
-        final ListMultimap<String, String> headers = BaseHttpMessage.Headers.create();
-        headers.put("X-Secret", "s3cr3t");
+        final Map<String, List<String>> headers = new BaseHttpMessage.HeadersBuilder()
+            .put("X-Secret", "s3cr3t")
+            .put("X-Secret", "knowledge")
+            .put("Y-Secret", Arrays.asList("one", "two"))
+            .build();
+
         assertThat(headers.get("x-secret"), hasItem("s3cr3t"));
+        assertThat(headers.get("x-secret"), hasItem("knowledge"));
+        assertThat(headers.get("Y-SECRET"), hasItem("one"));
+        assertThat(headers.get("Y-SECRET"), hasItem("two"));
     }
-
-}
\ No newline at end of file
+}
diff --git a/logbook-api/src/test/java/org/zalando/logbook/BodyObfuscatorTest.java b/logbook-api/src/test/java/org/zalando/logbook/BodyObfuscatorTest.java
index 03d908a0..827fffb0 100644
--- a/logbook-api/src/test/java/org/zalando/logbook/BodyObfuscatorTest.java
+++ b/logbook-api/src/test/java/org/zalando/logbook/BodyObfuscatorTest.java
@@ -2,7 +2,7 @@ package org.zalando.logbook;
 
 /*
  * #%L
- * Logbook: Core
+ * Logbook: API
  * %%
  * Copyright (C) 2015 Zalando SE
  * %%
@@ -35,4 +35,4 @@ public final class BodyObfuscatorTest {
         assertThat(unit.obfuscate("text/plain", "Hello, world!"), is(equalTo("Hello, world!")));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-api/src/test/java/org/zalando/logbook/EnforceCoverageTest.java b/logbook-api/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
index 5341d0c2..e38ee4d9 100644
--- a/logbook-api/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
+++ b/logbook-api/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
@@ -42,7 +42,7 @@ public final class EnforceCoverageTest {
 
     @Test
     public void shouldUseHeadersConstructor() {
-        new BaseHttpMessage.Headers();
+        new BaseHttpMessage.HeadersBuilder();
     }
 
     @Test
@@ -64,5 +64,4 @@ public final class EnforceCoverageTest {
     public void fakeLogbookShouldThrow() throws IOException {
         Logbook.create().write(mock(RawHttpRequest.class));
     }
-
 }
diff --git a/logbook-api/src/test/java/org/zalando/logbook/HeaderObfuscatorTest.java b/logbook-api/src/test/java/org/zalando/logbook/HeaderObfuscatorTest.java
index 52ea4784..d7995f75 100644
--- a/logbook-api/src/test/java/org/zalando/logbook/HeaderObfuscatorTest.java
+++ b/logbook-api/src/test/java/org/zalando/logbook/HeaderObfuscatorTest.java
@@ -2,7 +2,7 @@ package org.zalando.logbook;
 
 /*
  * #%L
- * Logbook: Core
+ * Logbook: API
  * %%
  * Copyright (C) 2015 Zalando SE
  * %%
@@ -36,4 +36,4 @@ public final class HeaderObfuscatorTest {
                 is(equalTo("Bearer c61a8f84-6834-11e5-a607-10ddb1ee7671")));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-api/src/test/java/org/zalando/logbook/HttpLogWriterTest.java b/logbook-api/src/test/java/org/zalando/logbook/HttpLogWriterTest.java
index 639a869a..2defbcb1 100644
--- a/logbook-api/src/test/java/org/zalando/logbook/HttpLogWriterTest.java
+++ b/logbook-api/src/test/java/org/zalando/logbook/HttpLogWriterTest.java
@@ -2,7 +2,7 @@ package org.zalando.logbook;
 
 /*
  * #%L
- * Logbook: Core
+ * Logbook: API
  * %%
  * Copyright (C) 2015 Zalando SE
  * %%
@@ -38,4 +38,4 @@ public final class HttpLogWriterTest {
         assertThat(unit.isActive(mock(RawHttpRequest.class)), is(true));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-api/src/test/java/org/zalando/logbook/HttpMessageTest.java b/logbook-api/src/test/java/org/zalando/logbook/HttpMessageTest.java
index 7c7c238b..5636e674 100644
--- a/logbook-api/src/test/java/org/zalando/logbook/HttpMessageTest.java
+++ b/logbook-api/src/test/java/org/zalando/logbook/HttpMessageTest.java
@@ -2,7 +2,7 @@ package org.zalando.logbook;
 
 /*
  * #%L
- * Logbook: Core
+ * Logbook: API
  * %%
  * Copyright (C) 2015 Zalando SE
  * %%
@@ -43,4 +43,4 @@ public final class HttpMessageTest {
         assertThat(message.getBodyAsString(), is("foo"));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/main/java/org/zalando/logbook/io/ByteStreams.java b/logbook-core/src/main/java/org/zalando/logbook/io/ByteStreams.java
new file mode 100644
index 00000000..6066ffb2
--- /dev/null
+++ b/logbook-core/src/main/java/org/zalando/logbook/io/ByteStreams.java
@@ -0,0 +1,53 @@
+package org.zalando.logbook.io;
+
+/*
+ * #%L
+ * Logbook: Core
+ * %%
+ * Copyright (C) 2015 - 2016 Zalando SE
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Objects;
+
+public final class ByteStreams {
+
+    ByteStreams() {
+        // package private so we can trick code coverage
+    }
+
+    public static byte[] toByteArray(final InputStream in) throws IOException {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        copy(in, out);
+        return out.toByteArray();
+    }
+
+    public static void copy(final InputStream from, final OutputStream to) throws IOException {
+        Objects.requireNonNull(from);
+        Objects.requireNonNull(to);
+        final byte[] buf = new byte[4096];
+        while (true) {
+            final int r = from.read(buf);
+            if (r == -1) {
+                break;
+            }
+            to.write(buf, 0, r);
+        }
+    }
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ConditionsTest.java b/logbook-core/src/test/java/org/zalando/logbook/ConditionsTest.java
index 6cbf76b5..afe8fe7a 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ConditionsTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ConditionsTest.java
@@ -20,12 +20,11 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
 import org.junit.Test;
 
 import java.util.function.Predicate;
 
-import static com.google.common.collect.Sets.newHashSet;
+import static java.util.Arrays.asList;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThat;
 import static org.zalando.logbook.Conditions.contentType;
@@ -34,9 +33,9 @@ import static org.zalando.logbook.Conditions.header;
 import static org.zalando.logbook.Conditions.requestTo;
 
 public final class ConditionsTest {
-    
+
     private final RawHttpRequest request = MockRawHttpRequest.request()
-            .headers(ImmutableListMultimap.of("X-Secret", "true"))
+            .headers(MockHeaders.of("X-Secret", "true"))
             .contentType("text/plain")
             .build();
 
@@ -46,14 +45,14 @@ public final class ConditionsTest {
 
         assertThat(unit.test(request), is(true));
     }
-    
+
     @Test
     public void excludeNotShouldMatchIfAnyMatches() {
         final Predicate<RawHttpRequest> unit = exclude(requestTo("/admin"), contentType("text/plain"));
 
         assertThat(unit.test(request), is(false));
     }
-    
+
     @Test
     public void excludeNotShouldMatchIfAllMatches() {
         final Predicate<RawHttpRequest> unit = exclude(requestTo("/"), contentType("text/plain"));
@@ -64,7 +63,7 @@ public final class ConditionsTest {
     @Test
     public void excludeShouldDefaultToAlwaysTrue() {
         final Predicate<RawHttpRequest> unit = exclude();
-        
+
         assertThat(unit.test(null), is(true));
     }
 
@@ -130,21 +129,21 @@ public final class ConditionsTest {
 
     @Test
     public void headerShouldMatchNameAndValuePredicate() {
-        final Predicate<RawHttpRequest> unit = header("X-Secret", newHashSet("true", "1")::contains);
+        final Predicate<RawHttpRequest> unit = header("X-Secret", asList("true", "1")::contains);
 
         assertThat(unit.test(request), is(true));
     }
 
     @Test
     public void headerShouldNotMatchNameAndValuePredicate() {
-        final Predicate<RawHttpRequest> unit = header("X-Secret", newHashSet("yes", "1")::contains);
+        final Predicate<RawHttpRequest> unit = header("X-Secret", asList("yes", "1")::contains);
 
         assertThat(unit.test(request), is(false));
     }
 
     @Test
     public void headerShouldMatchPredicate() {
-        final Predicate<RawHttpRequest> unit = header((name, value) -> 
+        final Predicate<RawHttpRequest> unit = header((name, value) ->
                 name.equalsIgnoreCase("X-Secret") && value.equalsIgnoreCase("true"));
 
         assertThat(unit.test(request), is(true));
@@ -152,10 +151,10 @@ public final class ConditionsTest {
 
     @Test
     public void headerShouldNotMatchPredicate() {
-        final Predicate<RawHttpRequest> unit = header((name, value) -> 
+        final Predicate<RawHttpRequest> unit = header((name, value) ->
                 name.equalsIgnoreCase("X-Secret") && value.equalsIgnoreCase("false"));
 
         assertThat(unit.test(request), is(false));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java b/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java
index c2171898..d315520b 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java
@@ -20,14 +20,13 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
 import org.junit.Test;
 import org.zalando.logbook.DefaultLogbook.SimpleCorrelation;
 import org.zalando.logbook.DefaultLogbook.SimplePrecorrelation;
 
 import java.io.IOException;
 
-import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThat;
 import static org.zalando.logbook.MockHttpRequest.request;
 import static org.zalando.logbook.MockHttpResponse.response;
@@ -44,7 +43,7 @@ public final class DefaultHttpLogFormatterTest {
                 .origin(Origin.REMOTE)
                 .path("/test")
                 .query("limit=1")
-                .headers(ImmutableListMultimap.of(
+                .headers(MockHeaders.of(
                         "Accept", "application/json",
                         "Content-Type", "text/plain"))
                 .body("Hello, world!")
@@ -52,7 +51,7 @@ public final class DefaultHttpLogFormatterTest {
 
         final String http = unit.format(new SimplePrecorrelation<>(correlationId, request));
 
-        assertThat(http, equalTo("Incoming Request: c9408eaa-677d-11e5-9457-10ddb1ee7671\n" +
+        assertThat(http, is("Incoming Request: c9408eaa-677d-11e5-9457-10ddb1ee7671\n" +
                 "GET http://localhost/test?limit=1 HTTP/1.0\n" +
                 "Accept: application/json\n" +
                 "Content-Type: text/plain\n" +
@@ -66,7 +65,7 @@ public final class DefaultHttpLogFormatterTest {
         final HttpRequest request = request()
                 .origin(Origin.LOCAL)
                 .path("/test")
-                .headers(ImmutableListMultimap.of(
+                .headers(MockHeaders.of(
                         "Accept", "application/json",
                         "Content-Type", "text/plain"))
                 .body("Hello, world!")
@@ -74,7 +73,7 @@ public final class DefaultHttpLogFormatterTest {
 
         final String http = unit.format(new SimplePrecorrelation<>(correlationId, request));
 
-        assertThat(http, equalTo("Outgoing Request: 2bd05240-6827-11e5-bbee-10ddb1ee7671\n" +
+        assertThat(http, is("Outgoing Request: 2bd05240-6827-11e5-bbee-10ddb1ee7671\n" +
                 "GET http://localhost/test HTTP/1.1\n" +
                 "Accept: application/json\n" +
                 "Content-Type: text/plain\n" +
@@ -87,12 +86,12 @@ public final class DefaultHttpLogFormatterTest {
         final String correlationId = "0eae9f6c-6824-11e5-8b0a-10ddb1ee7671";
         final HttpRequest request = request()
                 .path("/test")
-                .headers(ImmutableListMultimap.of("Accept", "application/json"))
+                .headers(MockHeaders.of("Accept", "application/json"))
                 .build();
 
         final String http = unit.format(new SimplePrecorrelation<>(correlationId, request));
 
-        assertThat(http, equalTo("Incoming Request: 0eae9f6c-6824-11e5-8b0a-10ddb1ee7671\n" +
+        assertThat(http, is("Incoming Request: 0eae9f6c-6824-11e5-8b0a-10ddb1ee7671\n" +
                 "GET http://localhost/test HTTP/1.1\n" +
                 "Accept: application/json"));
     }
@@ -104,13 +103,13 @@ public final class DefaultHttpLogFormatterTest {
         final HttpResponse response = response()
                 .protocolVersion("HTTP/1.0")
                 .origin(Origin.REMOTE)
-                .headers(ImmutableListMultimap.of("Content-Type", "application/json"))
+                .headers(MockHeaders.of("Content-Type", "application/json"))
                 .body("{\"success\":true}")
                 .build();
 
         final String http = unit.format(new SimpleCorrelation<>(correlationId, request, response));
 
-        assertThat(http, equalTo("Incoming Response: 2d51bc02-677e-11e5-8b9b-10ddb1ee7671\n" +
+        assertThat(http, is("Incoming Response: 2d51bc02-677e-11e5-8b9b-10ddb1ee7671\n" +
                 "HTTP/1.0 200\n" +
                 "Content-Type: application/json\n" +
                 "\n" +
@@ -123,14 +122,14 @@ public final class DefaultHttpLogFormatterTest {
         final HttpRequest request = MockHttpRequest.create();
         final HttpResponse response = response()
                 .origin(Origin.LOCAL)
-                .headers(ImmutableListMultimap.of("Content-Type", "application/json"))
+                .headers(MockHeaders.of("Content-Type", "application/json"))
                 .build();
 
         final String http = unit.format(new SimpleCorrelation<>(correlationId, request, response));
 
-        assertThat(http, equalTo("Outgoing Response: 3881ae92-6824-11e5-921b-10ddb1ee7671\n" +
+        assertThat(http, is("Outgoing Response: 3881ae92-6824-11e5-921b-10ddb1ee7671\n" +
                 "HTTP/1.1 200\n" +
                 "Content-Type: application/json"));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/EnforceCoverageTest.java b/logbook-core/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
index 24087b0a..db663c64 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
@@ -20,26 +20,6 @@ package org.zalando.logbook;
  * #L%
  */
 
-/*
- * ⁣​
- * Jackson-datatype-Problem
- * ⁣⁣
- * Copyright (C) 2015 Zalando SE
- * ⁣⁣
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * 
- *      http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ​⁣
- */
-
 import com.google.gag.annotation.remark.Hack;
 import com.google.gag.annotation.remark.OhNoYouDidnt;
 import org.junit.Test;
@@ -62,5 +42,4 @@ public final class EnforceCoverageTest {
     public void shouldUseObfuscatorsConstructor() {
         new Obfuscators();
     }
-
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpResponseTest.java b/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpResponseTest.java
index f1c1f05e..9ba03221 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpResponseTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpResponseTest.java
@@ -28,6 +28,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.emptyString;
 import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThat;
 import static org.zalando.logbook.Origin.LOCAL;
 
@@ -49,6 +50,7 @@ public final class ForwardingHttpResponseTest {
         assertThat(unit.getCharset(), is(UTF_8));
         assertThat(unit.getBody(), is("".getBytes(UTF_8)));
         assertThat(unit.getBodyAsString(), is(emptyString()));
+        assertNotNull(unit.toString());
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java b/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java
index ca2866f8..40fe2234 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java
@@ -20,15 +20,16 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableMultimap;
 import org.junit.Test;
 
 import java.io.IOException;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.emptyMap;
 import static org.hamcrest.Matchers.empty;
 import static org.hamcrest.Matchers.emptyString;
 import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThat;
 import static org.zalando.logbook.Origin.REMOTE;
 
@@ -55,9 +56,10 @@ public final class ForwardingRawHttpRequestTest {
         assertThat(unit.getPath(), is("/"));
         assertThat(unit.getQuery(), is(emptyString()));
         assertThat(unit.getProtocolVersion(), is("HTTP/1.1"));
-        assertThat(unit.getHeaders(), is(ImmutableMultimap.of()));
+        assertThat(unit.getHeaders(), is(emptyMap()));
         assertThat(unit.getContentType(), is(""));
         assertThat(unit.getCharset(), is(UTF_8));
+        assertNotNull(unit.toString());
     }
 
     @Test
@@ -81,4 +83,4 @@ public final class ForwardingRawHttpRequestTest {
         assertThat(request.getBodyAsString(), is(emptyString()));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java b/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java
index c6a4e5a7..4a4739ff 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java
@@ -20,7 +20,6 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
 import org.junit.Test;
 import org.zalando.logbook.DefaultLogbook.SimpleCorrelation;
 import org.zalando.logbook.DefaultLogbook.SimplePrecorrelation;
@@ -53,7 +52,7 @@ public final class JsonHttpLogFormatterTest {
                 .origin(REMOTE)
                 .path("/test")
                 .query("limit=1")
-                .headers(ImmutableListMultimap.of(
+                .headers(MockHeaders.of(
                         "Accept", "application/json",
                         "Date", "Tue, 15 Nov 1994 08:12:31 GMT"))
                 .contentType("application/xml")
@@ -158,6 +157,48 @@ public final class JsonHttpLogFormatterTest {
                 .assertThat("$.body.name", is("Bob"));
     }
 
+    @Test
+    public void shouldEmbedCustomJsonWithParametersRequestBodyAsIs() throws IOException {
+        final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
+        final HttpRequest request = request()
+                .contentType("application/custom+json; version=2")
+                .body("{\"name\":\"Bob\"}")
+                .build();
+
+        final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));
+
+        with(json)
+                .assertThat("$.body.name", is("Bob"));
+    }
+
+    @Test
+    public void shouldNotEmbedCustomTextXmlRequestBodyAsIs() throws IOException {
+        final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
+        final HttpRequest request = request()
+                .contentType("text/xml")
+                .body("{\"name\":\"Bob\"}")
+                .build();
+
+        final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));
+
+        with(json)
+                .assertThat("$.body", is("{\"name\":\"Bob\"}"));
+    }
+
+    @Test
+    public void shouldNotEmbedInvalidContentTypeRequestBodyAsIs() throws IOException {
+        final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
+        final HttpRequest request = request()
+                .contentType("x;y/z")
+                .body("{\"name\":\"Bob\"}")
+                .build();
+
+        final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));
+
+        with(json)
+                .assertThat("$.body", is("{\"name\":\"Bob\"}"));
+    }
+
     @Test
     public void shouldNotEmbedCustomTextJsonRequestBodyAsIs() throws IOException {
         final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
@@ -172,6 +213,20 @@ public final class JsonHttpLogFormatterTest {
                 .assertThat("$.body", is("{\"name\":\"Bob\"}"));
     }
 
+    @Test
+    public void shouldNotEmbedNonJsonRequestBodyAsIs() throws IOException {
+        final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
+        final HttpRequest request = request()
+                .contentType("application/jsonot")
+                .body("{\"name\":\"Bob\"}")
+                .build();
+
+        final String json = unit.format(new SimplePrecorrelation<>(correlationId, request));
+
+        with(json)
+                .assertThat("$.body", is("{\"name\":\"Bob\"}"));
+    }
+
     @Test
     public void shouldEmbedJsonRequestBodyAsNullIfEmpty() throws IOException {
         final String correlationId = "5478b8da-6d87-11e5-a80f-10ddb1ee7671";
@@ -192,7 +247,7 @@ public final class JsonHttpLogFormatterTest {
         final HttpResponse response = response()
                 .protocolVersion("HTTP/1.0")
                 .origin(LOCAL)
-                .headers(ImmutableListMultimap.of("Date", "Tue, 15 Nov 1994 08:12:31 GMT"))
+                .headers(MockHeaders.of("Date", "Tue, 15 Nov 1994 08:12:31 GMT"))
                 .contentType("application/xml")
                 .body("<success>true<success>")
                 .build();
@@ -326,4 +381,4 @@ public final class JsonHttpLogFormatterTest {
     }
 
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpRequestTest.java b/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpRequestTest.java
index 121e1e04..ed1ecad2 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpRequestTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpRequestTest.java
@@ -20,7 +20,6 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -38,7 +37,7 @@ public final class ObfuscatedHttpRequestTest {
 
     private final HttpRequest unit = new ObfuscatedHttpRequest(request()
             .query("password=1234&limit=1")
-            .headers(ImmutableListMultimap.of(
+            .headers(MockHeaders.of(
                     "Authorization", "Bearer 9b7606a6-6838-11e5-8ed4-10ddb1ee7671",
                     "Accept", "text/plain"))
             .body("My secret is s3cr3t")
@@ -67,12 +66,12 @@ public final class ObfuscatedHttpRequestTest {
 
     @Test
     public void shouldObfuscateAuthorizationHeader() {
-        assertThat(unit.getHeaders().asMap(), hasEntry(equalTo("Authorization"), contains("XXX")));
+        assertThat(unit.getHeaders(), hasEntry(equalTo("Authorization"), contains("XXX")));
     }
 
     @Test
     public void shouldNotObfuscateAcceptHeader() {
-        assertThat(unit.getHeaders().asMap(), hasEntry(equalTo("Accept"), contains("text/plain")));
+        assertThat(unit.getHeaders(), hasEntry(equalTo("Accept"), contains("text/plain")));
     }
 
     @Test
@@ -102,4 +101,4 @@ public final class ObfuscatedHttpRequestTest {
         assertThat(new String(unit.getBody(), unit.getCharset()), is("My secret is f4k3"));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpResponseTest.java b/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpResponseTest.java
index 406689d1..5a1d74d1 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpResponseTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ObfuscatedHttpResponseTest.java
@@ -20,7 +20,6 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
 import org.junit.Test;
 
 import java.io.IOException;
@@ -35,7 +34,7 @@ import static org.zalando.logbook.MockHttpResponse.response;
 public final class ObfuscatedHttpResponseTest {
 
     private final HttpResponse unit = new ObfuscatedHttpResponse(response()
-            .headers(ImmutableListMultimap.of(
+            .headers(MockHeaders.of(
                     "Authorization", "Bearer 9b7606a6-6838-11e5-8ed4-10ddb1ee7671",
                     "Accept", "text/plain"))
             .body("My secret is s3cr3t")
@@ -45,12 +44,12 @@ public final class ObfuscatedHttpResponseTest {
 
     @Test
     public void shouldObfuscateAuthorizationHeader() {
-        assertThat(unit.getHeaders().asMap(), hasEntry(equalTo("Authorization"), contains("XXX")));
+        assertThat(unit.getHeaders(), hasEntry(equalTo("Authorization"), contains("XXX")));
     }
 
     @Test
     public void shouldNotObfuscateAcceptHeader() {
-        assertThat(unit.getHeaders().asMap(), hasEntry(equalTo("Accept"), contains("text/plain")));
+        assertThat(unit.getHeaders(), hasEntry(equalTo("Accept"), contains("text/plain")));
     }
 
     @Test
@@ -63,4 +62,4 @@ public final class ObfuscatedHttpResponseTest {
         assertThat(new String(unit.getBody(), unit.getCharset()), is("My secret is f4k3"));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ObfuscatorsTest.java b/logbook-core/src/test/java/org/zalando/logbook/ObfuscatorsTest.java
index 4bf639cc..8d3774e2 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ObfuscatorsTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ObfuscatorsTest.java
@@ -22,9 +22,15 @@ package org.zalando.logbook;
 
 import org.junit.Test;
 
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.singletonList;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
 
 public final class ObfuscatorsTest {
 
@@ -59,4 +65,17 @@ public final class ObfuscatorsTest {
         assertThat(unit.obfuscate("Accept", "text/plain"), is(equalTo("text/plain")));
     }
 
-}
\ No newline at end of file
+    @Test
+    public void shouldObfuscateHeaders() {
+        final Map<String, List<String>> m = Obfuscators.obfuscateHeaders(
+            MockHeaders.of("hello", "world", "chao", "bambina"),
+            (k, v) -> v.toUpperCase()
+        );
+
+        assertEquals(2, m.size());
+        assertTrue(m.containsKey("hello"));
+        assertTrue(m.containsKey("chao"));
+        assertEquals(singletonList("WORLD"), m.get("hello"));
+        assertEquals(singletonList("BAMBINA"), m.get("chao"));
+    }
+}
diff --git a/logbook-core/src/test/java/org/zalando/logbook/io/ByteStreamsTest.java b/logbook-core/src/test/java/org/zalando/logbook/io/ByteStreamsTest.java
new file mode 100644
index 00000000..74478edd
--- /dev/null
+++ b/logbook-core/src/test/java/org/zalando/logbook/io/ByteStreamsTest.java
@@ -0,0 +1,50 @@
+package org.zalando.logbook.io;
+
+/*
+ * #%L
+ * Logbook: Core
+ * %%
+ * Copyright (C) 2015 - 2016 Zalando SE
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+public final class ByteStreamsTest {
+
+    @Test
+    public void shouldCollectStreamToByteArray() throws IOException {
+        final byte[] bytes = ByteStreams.toByteArray(new ByteArrayInputStream("Hello World!".getBytes(UTF_8)));
+
+        assertThat(new String(bytes, UTF_8), is("Hello World!"));
+    }
+
+    @Test
+    public void shouldCopyStreams() throws IOException {
+        final ByteArrayOutputStream to = new ByteArrayOutputStream();
+        ByteStreams.copy(new ByteArrayInputStream("Hello World!".getBytes(UTF_8)), to);
+
+        assertThat(new String(to.toByteArray(), UTF_8), is("Hello World!"));
+    }
+
+}
\ No newline at end of file
diff --git a/logbook-test/src/test/java/org/zalando/logbook/EnforceCoverageTest.java b/logbook-core/src/test/java/org/zalando/logbook/io/EnforceCoverageTest.java
similarity index 82%
rename from logbook-test/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
rename to logbook-core/src/test/java/org/zalando/logbook/io/EnforceCoverageTest.java
index 53444e49..5ab78f60 100644
--- a/logbook-test/src/test/java/org/zalando/logbook/EnforceCoverageTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/io/EnforceCoverageTest.java
@@ -1,10 +1,10 @@
-package org.zalando.logbook;
+package org.zalando.logbook.io;
 
 /*
  * #%L
- * Logbook: API
+ * Logbook: Core
  * %%
- * Copyright (C) 2015 - 2016 Zalando SE
+ * Copyright (C) 2015 Zalando SE
  * %%
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,8 +29,8 @@ import org.junit.Test;
 public final class EnforceCoverageTest {
 
     @Test
-    public void shouldUseMockHeadersConstructor() {
-        new MockHeaders();
+    public void shouldUseByteStreamsConstructor() {
+        new ByteStreams();
     }
 
 }
diff --git a/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/EnforceCoverageTest.java b/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/EnforceCoverageTest.java
index 96d5f2cf..b0e67072 100644
--- a/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/EnforceCoverageTest.java
+++ b/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/EnforceCoverageTest.java
@@ -20,26 +20,6 @@ package org.zalando.logbook.httpclient;
  * #L%
  */
 
-/*
- * ⁣​
- * Jackson-datatype-Problem
- * ⁣⁣
- * Copyright (C) 2015 Zalando SE
- * ⁣⁣
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * 
- *      http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ​⁣
- */
-
 import com.google.gag.annotation.remark.Hack;
 import com.google.gag.annotation.remark.OhNoYouDidnt;
 import org.junit.Test;
@@ -53,4 +33,4 @@ public final class EnforceCoverageTest {
         new Attributes();
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LocalRequestTest.java b/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LocalRequestTest.java
index 9d578831..237609f3 100644
--- a/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LocalRequestTest.java
+++ b/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LocalRequestTest.java
@@ -42,7 +42,6 @@ import java.net.URISyntaxException;
 import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 
-import static com.google.common.io.ByteStreams.toByteArray;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.hamcrest.Matchers.aMapWithSize;
 import static org.hamcrest.Matchers.hasSize;
@@ -54,6 +53,7 @@ import static org.hobsoft.hamcrest.compose.ComposeMatchers.hasFeature;
 import static org.junit.Assert.assertThat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.zalando.logbook.io.ByteStreams.toByteArray;
 
 public final class LocalRequestTest {
 
@@ -77,10 +77,10 @@ public final class LocalRequestTest {
     @Test
     public void shouldResolveLocalhost() {
         final LocalRequest unit = new LocalRequest(get("/"), Localhost.resolve());
-        
+
         assertThat(unit.getRemote(), matchesPattern("(\\d{1,3}\\.){3}\\d{1,3}"));
     }
-    
+
     @Test
     public void shouldHandleUnknownHostException() throws UnknownHostException {
         when(localhost.getAddress()).thenThrow(new UnknownHostException());
@@ -100,18 +100,18 @@ public final class LocalRequestTest {
     @Test
     public void shouldTrimQueryStringFromRequestUri() {
         final LocalRequest unit = unit(get("http://localhost/?limit=1"));
-        
+
         assertThat(unit, hasFeature("request uri", BaseHttpRequest::getRequestUri,
                 hasToString("http://localhost/?limit=1")));
     }
-    
+
     @Test
     public void shouldParseQueryStringIntoQueryParameters() {
         final LocalRequest unit = unit(get("http://localhost/?limit=1"));
-        
+
         assertThat(unit, hasFeature("query parameters", BaseHttpRequest::getQuery, is("limit=1")));
     }
-    
+
     @Test
     public void shouldRetrieveAbsoluteRequestUriForWrappedRequests() throws URISyntaxException {
         final LocalRequest unit = unit(wrap(get("http://localhost/")));
@@ -132,7 +132,7 @@ public final class LocalRequestTest {
         wrap.setURI(URIUtils.rewriteURIForRoute(URI.create("http://localhost/"), new HttpRoute(target)));
         return wrap;
     }
-    
+
     @Test
     public void shouldReturnContentTypesCharsetIfGiven() {
         final HttpRequest delegate = get("/");
@@ -146,7 +146,7 @@ public final class LocalRequestTest {
         final HttpRequest delegate = get("/");
         delegate.addHeader("Content-Type", "text/plain;");
         final LocalRequest unit = unit(delegate);
-        assertThat(unit.getHeaders().asMap(), aMapWithSize(1));
+        assertThat(unit.getHeaders(), aMapWithSize(1));
     }
 
     @Test
@@ -155,7 +155,7 @@ public final class LocalRequestTest {
         delegate.addHeader("Content-Type", "text/plain;");
         delegate.addHeader("Content-Type", "text/plain;");
         final LocalRequest unit = unit(delegate);
-        assertThat(unit.getHeaders().asMap(), aMapWithSize(1));
+        assertThat(unit.getHeaders(), aMapWithSize(1));
         assertThat(unit.getHeaders().get("Content-Type"), hasSize(2));
     }
 
@@ -164,7 +164,7 @@ public final class LocalRequestTest {
         final LocalRequest unit = unit(get("/"));
         assertThat(unit.getCharset(), is(UTF_8));
     }
-    
+
     @Test
     public void shouldReadBodyIfPresent() throws IOException {
         final HttpEntityEnclosingRequest delegate = post("/");
@@ -176,4 +176,4 @@ public final class LocalRequestTest {
         assertThat(new String(toByteArray(delegate.getEntity().getContent()), UTF_8), is("Hello, world!"));
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LogbookHttpInterceptorsTest.java b/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LogbookHttpInterceptorsTest.java
index 0b0cc45c..189dea4e 100644
--- a/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LogbookHttpInterceptorsTest.java
+++ b/logbook-httpclient/src/test/java/org/zalando/logbook/httpclient/LogbookHttpInterceptorsTest.java
@@ -39,7 +39,6 @@ import java.io.IOException;
 
 import static com.github.restdriver.clientdriver.RestClientDriver.giveResponse;
 import static com.github.restdriver.clientdriver.RestClientDriver.onRequestTo;
-import static com.google.common.io.ByteStreams.toByteArray;
 import static java.lang.String.format;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.hamcrest.Matchers.containsString;
@@ -51,6 +50,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.zalando.logbook.io.ByteStreams.toByteArray;
 
 public final class LogbookHttpInterceptorsTest {
 
@@ -93,9 +93,9 @@ public final class LogbookHttpInterceptorsTest {
     @Test
     public void shouldNotLogRequestIfInactive() throws IOException {
         when(writer.isActive(any())).thenReturn(false);
-        
+
         sendAndReceive();
-        
+
         verify(writer, never()).writeRequest(any());
     }
 
@@ -117,9 +117,9 @@ public final class LogbookHttpInterceptorsTest {
     @Test
     public void shouldNotLogResponseIfInactive() throws IOException {
         when(writer.isActive(any())).thenReturn(false);
-        
+
         sendAndReceive();
-        
+
         verify(writer, never()).writeResponse(any());
     }
 
@@ -133,4 +133,4 @@ public final class LogbookHttpInterceptorsTest {
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/AsyncDispatchTest.java b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/AsyncDispatchTest.java
index d826655c..6e23e507 100644
--- a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/AsyncDispatchTest.java
+++ b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/AsyncDispatchTest.java
@@ -20,12 +20,12 @@ package org.zalando.logbook.servlet;
  * #L%
  */
 
-import com.google.common.collect.ImmutableMultimap;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.zalando.logbook.BaseHttpMessage;
 import org.zalando.logbook.Correlation;
 import org.zalando.logbook.DefaultHttpLogFormatter;
 import org.zalando.logbook.HttpLogFormatter;
@@ -41,6 +41,7 @@ import javax.servlet.DispatcherType;
 import java.io.IOException;
 
 import static com.jayway.jsonassert.JsonAssert.with;
+import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static org.hamcrest.Matchers.emptyOrNullString;
 import static org.hamcrest.Matchers.hasEntry;
@@ -94,7 +95,7 @@ public final class AsyncDispatchTest {
         assertThat(request, hasFeature("method", HttpRequest::getMethod, is("GET")));
         assertThat(request, hasFeature("url", HttpRequest::getRequestUri,
                 hasToString("http://localhost/api/async")));
-        assertThat(request, hasFeature("headers", HttpRequest::getHeaders, is(ImmutableMultimap.of())));
+        assertThat(request, hasFeature("headers", HttpRequest::getHeaders, is(emptyMap())));
         assertThat(request, hasFeature("body", this::getBodyAsString, is(emptyOrNullString())));
     }
 
@@ -107,7 +108,7 @@ public final class AsyncDispatchTest {
         final HttpResponse response = interceptResponse();
 
         assertThat(response, hasFeature("status", HttpResponse::getStatus, is(200)));
-        assertThat(response, hasFeature("headers", r -> r.getHeaders().asMap(),
+        assertThat(response, hasFeature("headers", BaseHttpMessage::getHeaders,
                 hasEntry("Content-Type", singletonList("application/json"))));
         assertThat(response, hasFeature("content type", HttpResponse::getContentType, is("application/json")));
 
diff --git a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/EnforceCoverageTest.java b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/EnforceCoverageTest.java
index c301d919..959a359b 100644
--- a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/EnforceCoverageTest.java
+++ b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/EnforceCoverageTest.java
@@ -20,26 +20,6 @@ package org.zalando.logbook.servlet;
  * #L%
  */
 
-/*
- * ⁣​
- * Jackson-datatype-Problem
- * ⁣⁣
- * Copyright (C) 2015 Zalando SE
- * ⁣⁣
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- * 
- *      http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ​⁣
- */
-
 import com.google.gag.annotation.remark.Hack;
 import com.google.gag.annotation.remark.OhNoYouDidnt;
 import org.junit.Test;
@@ -57,20 +37,20 @@ public final class EnforceCoverageTest {
     public void shouldCreateLogbookFilter() {
         new LogbookFilter();
     }
-    
+
     @Test
     public void shouldCallInit() throws ServletException {
         new LogbookFilter().init(mock(FilterConfig.class));
     }
-    
+
     @Test
     public void shouldCallDestroy() {
         new LogbookFilter().destroy();
     }
-    
+
     @Test
     public void shouldUseAttributesConstructor() {
         new Attributes();
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ErrorDispatchTest.java b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ErrorDispatchTest.java
index ce635c04..1448f1cb 100644
--- a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ErrorDispatchTest.java
+++ b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ErrorDispatchTest.java
@@ -20,7 +20,6 @@ package org.zalando.logbook.servlet;
  * #L%
  */
 
-import com.google.common.collect.ImmutableMultimap;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -40,6 +39,7 @@ import org.zalando.logbook.servlet.example.ExampleController;
 import javax.servlet.DispatcherType;
 import java.io.IOException;
 
+import static java.util.Collections.emptyMap;
 import static org.hamcrest.Matchers.is;
 import static org.hobsoft.hamcrest.compose.ComposeMatchers.hasFeature;
 import static org.junit.Assert.assertThat;
@@ -82,7 +82,7 @@ public final class ErrorDispatchTest {
         final HttpResponse response = interceptResponse();
 
         assertThat(response, hasFeature("status", HttpResponse::getStatus, is(404)));
-        assertThat(response, hasFeature("headers", HttpResponse::getHeaders, is(ImmutableMultimap.of())));
+        assertThat(response, hasFeature("headers", HttpResponse::getHeaders, is(emptyMap())));
     }
 
     private String getBodyAsString(final HttpMessage message) {
diff --git a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/FormattingTest.java b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/FormattingTest.java
index 5336a26f..c2ed6b4c 100644
--- a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/FormattingTest.java
+++ b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/FormattingTest.java
@@ -26,6 +26,7 @@ import org.mockito.ArgumentCaptor;
 import org.springframework.http.MediaType;
 import org.springframework.test.web.servlet.MockMvc;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.zalando.logbook.BaseHttpMessage;
 import org.zalando.logbook.Correlation;
 import org.zalando.logbook.DefaultHttpLogFormatter;
 import org.zalando.logbook.HttpLogFormatter;
@@ -39,9 +40,9 @@ import org.zalando.logbook.servlet.example.ExampleController;
 
 import java.io.IOException;
 
-import static com.google.common.collect.ImmutableMultimap.of;
 import static com.jayway.jsonassert.JsonAssert.with;
 import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
 import static org.hamcrest.Matchers.emptyString;
 import static org.hamcrest.Matchers.hasEntry;
 import static org.hamcrest.Matchers.hasSize;
@@ -87,13 +88,14 @@ public final class FormattingTest {
                 .accept(MediaType.TEXT_PLAIN));
 
         final HttpRequest request = interceptRequest();
-        
+
         assertThat(request, hasFeature("remote address", HttpRequest::getRemote, is("127.0.0.1")));
         assertThat(request, hasFeature("method", HttpRequest::getMethod, is("GET")));
         assertThat(request, hasFeature("url", HttpRequest::getRequestUri,
                 hasToString("http://localhost/api/sync?limit=1")));
         assertThat(request, hasFeature("parameters", HttpRequest::getQuery, is("limit=1")));
-        assertThat(request, hasFeature("headers", HttpRequest::getHeaders, is(of("Accept", "text/plain"))));
+        assertThat(request, hasFeature("headers", HttpRequest::getHeaders,
+                is(singletonMap("Accept", singletonList("text/plain")))));
         assertThat(request, hasFeature("body", this::getBody, is(notNullValue())));
         assertThat(request, hasFeature("body", this::getBodyAsString, is(emptyString())));
     }
@@ -105,7 +107,7 @@ public final class FormattingTest {
         final HttpResponse response = interceptResponse();
 
         assertThat(response, hasFeature("status", HttpResponse::getStatus, is(200)));
-        assertThat(response, hasFeature("headers", r -> r.getHeaders().asMap(),
+        assertThat(response, hasFeature("headers", BaseHttpMessage::getHeaders,
                 hasEntry("Content-Type", singletonList("application/json"))));
         assertThat(response, hasFeature("content type", HttpResponse::getContentType, is("application/json")));
 
diff --git a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ForwardingHttpLogFormatter.java b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ForwardingHttpLogFormatter.java
index 154c8a0e..3babb443 100644
--- a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ForwardingHttpLogFormatter.java
+++ b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/ForwardingHttpLogFormatter.java
@@ -20,7 +20,6 @@ package org.zalando.logbook.servlet;
  * #L%
  */
 
-import com.google.common.collect.ForwardingObject;
 import org.zalando.logbook.Correlation;
 import org.zalando.logbook.HttpLogFormatter;
 import org.zalando.logbook.HttpRequest;
@@ -30,7 +29,7 @@ import org.zalando.logbook.Precorrelation;
 import java.io.IOException;
 
 // non final so we can wrap a spy around it
-class ForwardingHttpLogFormatter extends ForwardingObject implements HttpLogFormatter {
+class ForwardingHttpLogFormatter implements HttpLogFormatter {
 
     private final HttpLogFormatter formatter;
 
@@ -38,7 +37,6 @@ class ForwardingHttpLogFormatter extends ForwardingObject implements HttpLogForm
         this.formatter = formatter;
     }
 
-    @Override
     protected HttpLogFormatter delegate() {
         return formatter;
     }
@@ -53,4 +51,8 @@ class ForwardingHttpLogFormatter extends ForwardingObject implements HttpLogForm
         return delegate().format(correlation);
     }
 
+    @Override
+    public String toString() {
+        return delegate().toString();
+    }
 }
diff --git a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/example/ExampleController.java b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/example/ExampleController.java
index a8a9e7eb..234497da 100644
--- a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/example/ExampleController.java
+++ b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/example/ExampleController.java
@@ -20,13 +20,12 @@ package org.zalando.logbook.servlet.example;
  * #L%
  */
 
-import com.google.common.io.ByteStreams;
-import com.google.common.io.CharStreams;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.zalando.logbook.io.ByteStreams;
 
 import javax.servlet.ServletInputStream;
 import javax.servlet.ServletOutputStream;
@@ -34,6 +33,8 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.nio.CharBuffer;
+import java.util.Objects;
 import java.util.concurrent.Callable;
 
 @RestController
@@ -66,13 +67,13 @@ public class ExampleController {
     public void error() {
         throw new UnsupportedOperationException();
     }
-    
+
     @RequestMapping(value = "/read-byte", produces = MediaType.TEXT_PLAIN_VALUE)
     public void readByte(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
 
         final ServletInputStream input = request.getInputStream();
         final ServletOutputStream output = response.getOutputStream();
-        
+
         while (true) {
             final int read = input.read();
             if (read == -1) {
@@ -81,7 +82,7 @@ public class ExampleController {
             output.write(read);
         }
     }
-    
+
     @RequestMapping(value = "/read-bytes", produces = MediaType.TEXT_PLAIN_VALUE)
     public void readBytes(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
 
@@ -89,7 +90,7 @@ public class ExampleController {
         final ServletOutputStream output = response.getOutputStream();
 
         final byte[] buffer = new byte[1];
-        
+
         while (true) {
             final int read = input.read(buffer);
             if (read == -1) {
@@ -98,16 +99,16 @@ public class ExampleController {
             output.write(buffer);
         }
     }
-    
+
     @RequestMapping(value = "/stream", produces = MediaType.TEXT_PLAIN_VALUE)
     public void stream(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
         ByteStreams.copy(request.getInputStream(), response.getOutputStream());
     }
-    
+
     @RequestMapping(value = "/reader", produces = MediaType.TEXT_PLAIN_VALUE)
     public void reader(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
         try (PrintWriter writer = response.getWriter()) {
-            CharStreams.copy(request.getReader(), writer);
+            copy(request.getReader(), writer);
         }
     }
 
@@ -120,4 +121,17 @@ public class ExampleController {
         };
     }
 
+    private static long copy(final Readable from, final Appendable to) throws IOException {
+        Objects.requireNonNull(from);
+        Objects.requireNonNull(to);
+        final CharBuffer buf = CharBuffer.allocate(0x800);
+        long total = 0;
+        while (from.read(buf) != -1) {
+            buf.flip();
+            to.append(buf);
+            total += buf.remaining();
+            buf.clear();
+        }
+        return total;
+    }
 }
diff --git a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/AbstractTest.java b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/AbstractTest.java
index 8e7d5139..7e2e6d5e 100644
--- a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/AbstractTest.java
+++ b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/AbstractTest.java
@@ -30,4 +30,4 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 @SpringApplicationConfiguration(Application.class)
 public abstract class AbstractTest {
 
-}
\ No newline at end of file
+}
diff --git a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ExcludeTest.java b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ExcludeTest.java
index 28d86725..00bc886f 100644
--- a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ExcludeTest.java
+++ b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ExcludeTest.java
@@ -56,7 +56,7 @@ public final class ExcludeTest extends AbstractTest {
             when(logger.isTraceEnabled()).thenReturn(true);
             return logger;
         }
-        
+
         @Bean
         public Predicate<RawHttpRequest> condition() {
             return exclude(requestTo("/health"));
@@ -101,4 +101,4 @@ public final class ExcludeTest extends AbstractTest {
                 .build();
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersCustomTest.java b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersCustomTest.java
index fd16983b..c7b8f06a 100644
--- a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersCustomTest.java
+++ b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersCustomTest.java
@@ -20,7 +20,6 @@ package org.zalando.logbook.spring;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,6 +29,7 @@ import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.TestPropertySource;
 import org.zalando.logbook.HttpLogWriter;
 import org.zalando.logbook.Logbook;
+import org.zalando.logbook.MockHeaders;
 import org.zalando.logbook.MockRawHttpRequest;
 import org.zalando.logbook.Precorrelation;
 import org.zalando.logbook.RawHttpRequest;
@@ -70,7 +70,7 @@ public final class ObfuscateHeadersCustomTest extends AbstractTest {
     @Test
     public void shouldObfuscateHeaders() throws IOException {
         final RawHttpRequest request = MockRawHttpRequest.request()
-                .headers(ImmutableListMultimap.of(
+                .headers(MockHeaders.of(
                         "Authorization", "123",
                         "X-Access-Token", "123",
                         "X-Trace-ID", "ABC"
@@ -93,4 +93,4 @@ public final class ObfuscateHeadersCustomTest extends AbstractTest {
         return ArgumentCaptor.forClass(Precorrelation.class);
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersDefaultTest.java b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersDefaultTest.java
index c629da01..b3a24401 100644
--- a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersDefaultTest.java
+++ b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateHeadersDefaultTest.java
@@ -20,7 +20,6 @@ package org.zalando.logbook.spring;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -30,6 +29,7 @@ import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.TestPropertySource;
 import org.zalando.logbook.HttpLogWriter;
 import org.zalando.logbook.Logbook;
+import org.zalando.logbook.MockHeaders;
 import org.zalando.logbook.MockRawHttpRequest;
 import org.zalando.logbook.Precorrelation;
 import org.zalando.logbook.RawHttpRequest;
@@ -68,7 +68,7 @@ public final class ObfuscateHeadersDefaultTest extends AbstractTest {
     @Test
     public void shouldObfuscateAuthorizationByDefault() throws IOException {
         final RawHttpRequest request = MockRawHttpRequest.request()
-                .headers(ImmutableListMultimap.of(
+                .headers(MockHeaders.of(
                         "Authorization", "123",
                         "X-Secret", "123"
                 ))
@@ -89,4 +89,4 @@ public final class ObfuscateHeadersDefaultTest extends AbstractTest {
         return ArgumentCaptor.forClass(Precorrelation.class);
     }
 
-}
\ No newline at end of file
+}
diff --git a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateRequestCustomTest.java b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateRequestCustomTest.java
index e9974b6c..07c04f9e 100644
--- a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateRequestCustomTest.java
+++ b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateRequestCustomTest.java
@@ -27,7 +27,6 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.TestPropertySource;
-import org.zalando.logbook.BodyObfuscator;
 import org.zalando.logbook.HttpLogWriter;
 import org.zalando.logbook.Logbook;
 import org.zalando.logbook.MockHttpRequest;
diff --git a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateResponseCustomTest.java b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateResponseCustomTest.java
index 5e4d130a..06766169 100644
--- a/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateResponseCustomTest.java
+++ b/logbook-spring-boot-starter/src/test/java/org/zalando/logbook/spring/ObfuscateResponseCustomTest.java
@@ -31,13 +31,9 @@ import org.zalando.logbook.Correlation;
 import org.zalando.logbook.Correlator;
 import org.zalando.logbook.HttpLogWriter;
 import org.zalando.logbook.Logbook;
-import org.zalando.logbook.MockHttpRequest;
 import org.zalando.logbook.MockHttpResponse;
 import org.zalando.logbook.MockRawHttpRequest;
 import org.zalando.logbook.MockRawHttpResponse;
-import org.zalando.logbook.Precorrelation;
-import org.zalando.logbook.RawHttpRequest;
-import org.zalando.logbook.RequestObfuscator;
 import org.zalando.logbook.ResponseObfuscator;
 
 import java.io.IOException;
diff --git a/logbook-spring-boot-starter/src/test/resources/simplelogger.properties b/logbook-spring-boot-starter/src/test/resources/simplelogger.properties
index ce629f76..e1d34cc9 100644
--- a/logbook-spring-boot-starter/src/test/resources/simplelogger.properties
+++ b/logbook-spring-boot-starter/src/test/resources/simplelogger.properties
@@ -1,15 +1,15 @@
 ###
 # #%L
-# Logbook: Spring Boot Starter
+# Logbook: Spring
 # %%
 # Copyright (C) 2015 Zalando SE
 # %%
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at
-# 
+#
 #      http://www.apache.org/licenses/LICENSE-2.0
-# 
+#
 # Unless required by applicable law or agreed to in writing, software
 # distributed under the License is distributed on an "AS IS" BASIS,
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockHeaders.java b/logbook-test/src/main/java/org/zalando/logbook/MockHeaders.java
index 94aa259c..4e27fc63 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockHeaders.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockHeaders.java
@@ -20,18 +20,32 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
+import java.util.List;
+import java.util.Map;
 
-final class MockHeaders {
+public final class MockHeaders {
 
     MockHeaders() {
         // package private so we can trick code coverage
     }
 
-    static ListMultimap<String, String> copy(final ListMultimap<String, String> headers) {
-        final ListMultimap<String, String> copy = BaseHttpMessage.Headers.create();
-        copy.putAll(headers);
-        return copy;
+    public static Map<String, List<String>> of(final String k1, final String v1) {
+        return buildHeaders(k1, v1);
     }
 
+    public static Map<String, List<String>> of(final String k1, final String v1, final String k2, final String v2) {
+        return buildHeaders(k1, v1, k2, v2);
+    }
+
+    public static Map<String, List<String>> of(final String k1, final String v1, final String k2, final String v2, final String k3, final String v3) {
+        return buildHeaders(k1, v1, k2, v2, k3, v3);
+    }
+
+    private static Map<String, List<String>> buildHeaders(final String... x) {
+        final BaseHttpMessage.HeadersBuilder builder = new BaseHttpMessage.HeadersBuilder();
+        for (int i = 0; i < x.length; i += 2) {
+            builder.put(x[i], x[i + 1]);
+        }
+        return builder.build();
+    }
 }
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockHttpMessage.java b/logbook-test/src/main/java/org/zalando/logbook/MockHttpMessage.java
index fcfbf3a3..b4c435db 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockHttpMessage.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockHttpMessage.java
@@ -20,17 +20,16 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ListMultimap;
-
 import javax.annotation.Nullable;
+import java.util.Map;
+import java.util.Objects;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 
 interface MockHttpMessage {
 
-    default  <K, V> ListMultimap<K, V> firstNonNullNorEmpty(@Nullable final ListMultimap<K, V> first,
-            final ListMultimap<K, V> second) {
-        return first != null && !first.isEmpty() ? first : checkNotNull(second);
+    default  <K, V> Map<K, V> firstNonNullNorEmpty(@Nullable final Map<K, V> first,
+            final Map<K, V> second) {
+        return first != null && !first.isEmpty() ? first : Objects.requireNonNull(second);
     }
 
 }
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java b/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java
index 71f9fc74..1d81ad76 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java
@@ -20,16 +20,15 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static org.zalando.logbook.MockHeaders.copy;
+import static java.util.Collections.emptyMap;
 
 @Immutable
 public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
@@ -43,7 +42,7 @@ public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
     private final int port;
     private final String path;
     private final String query;
-    private final ListMultimap<String, String> headers;
+    private final Map<String, List<String>> headers;
     private final String contentType;
     private final Charset charset;
     private final String body;
@@ -59,23 +58,23 @@ public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
             final int port,
             @Nullable final String path,
             @Nullable final String query,
-            @Nullable final ListMultimap<String, String> headers,
+            @Nullable final Map<String, List<String>> headers,
             @Nullable final String contentType,
             @Nullable final Charset charset,
             @Nullable final String body) {
-        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
-        this.origin = firstNonNull(origin, Origin.REMOTE);
-        this.remote = firstNonNull(remote, "127.0.0.1");
-        this.method = firstNonNull(method, "GET");
-        this.scheme = firstNonNull(scheme, "http");
-        this.host = firstNonNull(host, "localhost");
+        this.protocolVersion = Optional.ofNullable(protocolVersion).orElse("HTTP/1.1");
+        this.origin = Optional.ofNullable(origin).orElse(Origin.REMOTE);
+        this.remote = Optional.ofNullable(remote).orElse("127.0.0.1");
+        this.method = Optional.ofNullable(method).orElse("GET");
+        this.scheme = Optional.ofNullable(scheme).orElse("http");
+        this.host = Optional.ofNullable(host).orElse("localhost");
         this.port = port == 0 ? 80 : port;
-        this.path = firstNonNull(path, "/");
-        this.query = firstNonNull(query, "");
-        this.headers = copy(firstNonNullNorEmpty(headers, ImmutableListMultimap.of()));
-        this.contentType = firstNonNull(contentType, "");
-        this.charset = firstNonNull(charset, StandardCharsets.UTF_8);
-        this.body = firstNonNull(body, "");
+        this.path = Optional.ofNullable(path).orElse("/");
+        this.query = Optional.ofNullable(query).orElse("");
+        this.headers = firstNonNullNorEmpty(headers, emptyMap());
+        this.contentType = Optional.ofNullable(contentType).orElse("");
+        this.charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
+        this.body = Optional.ofNullable(body).orElse("");
     }
 
     @Override
@@ -124,7 +123,7 @@ public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return headers;
     }
 
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java b/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java
index b779b06b..3043d37d 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java
@@ -20,16 +20,15 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static org.zalando.logbook.MockHeaders.copy;
+import static java.util.Collections.emptyMap;
 
 @Immutable
 public final class MockHttpResponse implements MockHttpMessage, HttpResponse {
@@ -37,7 +36,7 @@ public final class MockHttpResponse implements MockHttpMessage, HttpResponse {
     private final String protocolVersion;
     private final Origin origin;
     private final int status;
-    private final ListMultimap<String, String> headers;
+    private final Map<String, List<String>> headers;
     private final String contentType;
     private final Charset charset;
     private final String body;
@@ -47,17 +46,17 @@ public final class MockHttpResponse implements MockHttpMessage, HttpResponse {
             @Nullable final String protocolVersion,
             @Nullable final Origin origin,
             final int status,
-            @Nullable final ListMultimap<String, String> headers,
+            @Nullable final Map<String, List<String>> headers,
             @Nullable final String contentType,
             @Nullable final Charset charset,
             @Nullable final String body) {
-        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
-        this.origin = firstNonNull(origin, Origin.LOCAL);
+        this.protocolVersion = Optional.ofNullable(protocolVersion).orElse("HTTP/1.1");
+        this.origin = Optional.ofNullable(origin).orElse(Origin.LOCAL);
         this.status = status == 0 ? 200 : status;
-        this.headers = copy(firstNonNullNorEmpty(headers, ImmutableListMultimap.of()));
-        this.contentType = firstNonNull(contentType, "");
-        this.charset = firstNonNull(charset, StandardCharsets.UTF_8);
-        this.body = firstNonNull(body, "");
+        this.headers = firstNonNullNorEmpty(headers, emptyMap());
+        this.contentType = Optional.ofNullable(contentType).orElse("");
+        this.charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
+        this.body = Optional.ofNullable(body).orElse("");
     }
 
     @Override
@@ -76,7 +75,7 @@ public final class MockHttpResponse implements MockHttpMessage, HttpResponse {
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return headers;
     }
 
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java
index 85105280..70427087 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java
@@ -20,17 +20,16 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static org.zalando.logbook.MockHeaders.copy;
+import static java.util.Collections.emptyMap;
 
 @Immutable
 public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest {
@@ -44,7 +43,7 @@ public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest
     private final int port;
     private final String path;
     private final String query;
-    private final ListMultimap<String, String> headers;
+    private final Map<String, List<String>> headers;
     private final String contentType;
     private final Charset charset;
     private final String body;
@@ -60,23 +59,23 @@ public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest
             final int port,
             @Nullable final String path,
             @Nullable final String query,
-            @Nullable final ListMultimap<String, String> headers,
+            @Nullable final Map<String, List<String>> headers,
             @Nullable final String contentType,
             @Nullable final Charset charset,
             @Nullable final String body) {
-        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
-        this.origin = firstNonNull(origin, Origin.REMOTE);
-        this.remote = firstNonNull(remote, "127.0.0.1");
-        this.method = firstNonNull(method, "GET");
-        this.scheme = firstNonNull(scheme, "http");
-        this.host = firstNonNull(host, "localhost");
+        this.protocolVersion = Optional.ofNullable(protocolVersion).orElse("HTTP/1.1");
+        this.origin = Optional.ofNullable(origin).orElse(Origin.REMOTE);
+        this.remote = Optional.ofNullable(remote).orElse("127.0.0.1");
+        this.method = Optional.ofNullable(method).orElse("GET");
+        this.scheme = Optional.ofNullable(scheme).orElse("http");
+        this.host = Optional.ofNullable(host).orElse("localhost");
         this.port = port == 0 ? 80 : port;
-        this.path = firstNonNull(path, "/");
-        this.query = firstNonNull(query, "");
-        this.headers = copy(firstNonNullNorEmpty(headers, ImmutableListMultimap.of()));
-        this.contentType = firstNonNull(contentType, "");
-        this.charset = firstNonNull(charset, StandardCharsets.UTF_8);
-        this.body = firstNonNull(body, "");
+        this.path = Optional.ofNullable(path).orElse("/");
+        this.query = Optional.ofNullable(query).orElse("");
+        this.headers = firstNonNullNorEmpty(headers, emptyMap());
+        this.contentType = Optional.ofNullable(contentType).orElse("");
+        this.charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
+        this.body = Optional.ofNullable(body).orElse("");
     }
 
     @Override
@@ -120,7 +119,7 @@ public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return headers;
     }
 
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java
index 60a7a213..898f0b1e 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java
@@ -20,17 +20,16 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
 
-import static com.google.common.base.MoreObjects.firstNonNull;
-import static org.zalando.logbook.MockHeaders.copy;
+import static java.util.Collections.emptyMap;
 
 @Immutable
 public final class MockRawHttpResponse implements MockHttpMessage, RawHttpResponse {
@@ -38,7 +37,7 @@ public final class MockRawHttpResponse implements MockHttpMessage, RawHttpRespon
     private final String protocolVersion;
     private final Origin origin;
     private final int status;
-    private final ListMultimap<String, String> headers;
+    private final Map<String, List<String>> headers;
     private final String contentType;
     private final Charset charset;
     private final String body;
@@ -48,17 +47,17 @@ public final class MockRawHttpResponse implements MockHttpMessage, RawHttpRespon
             @Nullable final String protocolVersion,
             @Nullable final Origin origin,
             final int status,
-            @Nullable final ListMultimap<String, String> headers,
+            @Nullable final Map<String, List<String>> headers,
             @Nullable final String contentType,
             @Nullable final Charset charset,
             @Nullable final String body) {
-        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
-        this.origin = firstNonNull(origin, Origin.LOCAL);
+        this.protocolVersion = Optional.ofNullable(protocolVersion).orElse("HTTP/1.1");
+        this.origin = Optional.ofNullable(origin).orElse(Origin.LOCAL);
         this.status = status == 0 ? 200 : status;
-        this.headers = copy(firstNonNullNorEmpty(headers, ImmutableListMultimap.of()));
-        this.contentType = firstNonNull(contentType, "");
-        this.charset = firstNonNull(charset, StandardCharsets.UTF_8);
-        this.body = firstNonNull(body, "");
+        this.headers = firstNonNullNorEmpty(headers, emptyMap());
+        this.contentType = Optional.ofNullable(contentType).orElse("");
+        this.charset = Optional.ofNullable(charset).orElse(StandardCharsets.UTF_8);
+        this.body = Optional.ofNullable(body).orElse("");
     }
 
     @Override
@@ -77,7 +76,7 @@ public final class MockRawHttpResponse implements MockHttpMessage, RawHttpRespon
     }
 
     @Override
-    public ListMultimap<String, String> getHeaders() {
+    public Map<String, List<String>> getHeaders() {
         return headers;
     }
 
diff --git a/logbook-test/src/test/java/org/zalando/logbook/MockHeadersTest.java b/logbook-test/src/test/java/org/zalando/logbook/MockHeadersTest.java
new file mode 100644
index 00000000..1fb96982
--- /dev/null
+++ b/logbook-test/src/test/java/org/zalando/logbook/MockHeadersTest.java
@@ -0,0 +1,64 @@
+package org.zalando.logbook;
+
+/*
+ * #%L
+ * Logbook: Test
+ * %%
+ * Copyright (C) 2015 - 2016 Zalando SE
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+
+public class MockHeadersTest {
+
+    @Test
+    public void satisfyCoverage() {
+        new MockHeaders();
+    }
+
+    @Test
+    public void testOf1() {
+        final Map<String, List<String>> m = MockHeaders.of("x", "y");
+
+        assertEquals(1, m.size());
+        assertEquals(m.get("x"), singletonList("y"));
+    }
+
+    @Test
+    public void testOf2() {
+        final Map<String, List<String>> m = MockHeaders.of("x", "y", "a", "b");
+
+        assertEquals(2, m.size());
+        assertEquals(m.get("x"), singletonList("y"));
+        assertEquals(m.get("a"), singletonList("b"));
+    }
+
+    @Test
+    public void testOf3() {
+        final Map<String, List<String>> m = MockHeaders.of("x", "y", "a", "b", "1", "2");
+
+        assertEquals(3, m.size());
+        assertEquals(m.get("x"), singletonList("y"));
+        assertEquals(m.get("a"), singletonList("b"));
+        assertEquals(m.get("1"), singletonList("2"));
+    }
+}
diff --git a/logbook-test/src/test/java/org/zalando/logbook/MockHttpMessageTest.java b/logbook-test/src/test/java/org/zalando/logbook/MockHttpMessageTest.java
index 0924d68f..e2dc1347 100644
--- a/logbook-test/src/test/java/org/zalando/logbook/MockHttpMessageTest.java
+++ b/logbook-test/src/test/java/org/zalando/logbook/MockHttpMessageTest.java
@@ -20,10 +20,12 @@ package org.zalando.logbook;
  * #L%
  */
 
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
 import org.junit.Test;
 
+import java.util.Map;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThat;
 import static org.mockito.Mockito.spy;
@@ -33,10 +35,10 @@ public final class MockHttpMessageTest {
     private final MockHttpMessage unit = spy(MockHttpMessage.class);
 
     @Test
-    public void shouldSelectNonEmptyMultimap() {
-        final ImmutableListMultimap<Object, Object> expected = ImmutableListMultimap.of("foo", "bar");
-        final ListMultimap<Object, Object> actual = unit.firstNonNullNorEmpty(expected, ImmutableListMultimap.of());
+    public void shouldSelectNonEmptyMmap() {
+        final Map<Object, Object> expected = singletonMap("foo", "bar");
+        final Map<Object, Object> actual = unit.firstNonNullNorEmpty(expected, emptyMap());
         assertThat(actual, is(expected));
     }
 
-}
\ No newline at end of file
+}