Merge branch 'stable-2.15'

* stable-2.15:
  Allow storing audits into a separate file
  Extract audit configuration into a separate class
  Rename 'renderer' to 'format'
  Move CSV-specific classes into CSV-renderer
  Support audit events rendered as JSON string
  Extract Guice Module into a new file
  Extract the object o string rendering engine
  Extract AuditWriter and make the plugin testable
  Fix audit object class to RpcAuditEvent

Change-Id: I6105cc65622721bf688340b4ac706577732b7ff9
diff --git a/BUILD b/BUILD
index f75e83a..2dec1e5 100644
--- a/BUILD
+++ b/BUILD
@@ -12,9 +12,32 @@
     manifest_entries = [
         "Gerrit-PluginName: audit-sl4j",
         "Gerrit-ReloadMode: reload",
+        "Gerrit-Module: com.googlesource.gerrit.plugins.auditsl4j.Module",
         "Implementation-Title: Gerrit Audit provider for SLF4J",
         "Implementation-URL: https://gerrit.googlesource.com/plugins/audit-sl4j/",
     ],
     resources = glob(["src/main/resources/**/*"]),
     deps = [ ],
 )
+
+junit_tests(
+    name = "audit_sl4j_tests",
+    srcs = glob(["src/test/java/**/*Test.java"]),
+    visibility = ["//visibility:public"],
+    deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
+        ":audit-sl4j__plugin",
+        ":audit_sl4j_util",
+    ],
+)
+
+java_library(
+    name = "audit_sl4j_util",
+    testonly = 1,
+    srcs = glob(
+        ["src/test/java/**/*.java"],
+        exclude = ["src/test/java/**/*Test.java"],
+    ),
+    deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
+        ":audit-sl4j__plugin",
+    ],
+)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditConfig.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditConfig.java
new file mode 100644
index 0000000..291c1f3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditConfig.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.inject.Inject;
+import java.util.Optional;
+
+public class AuditConfig {
+  private final PluginConfig config;
+
+  @Inject
+  public AuditConfig(@PluginName String pluginName, PluginConfigFactory configFactory) {
+    config = configFactory.getFromGerritConfig(pluginName);
+  }
+
+  public AuditFormatTypes getFormat() {
+    return config.getEnum("format", AuditFormatTypes.CSV);
+  }
+
+  public Optional<String> getLogName() {
+    return Optional.ofNullable(config.getString("logName"));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatRenderer.java
similarity index 73%
rename from src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java
rename to src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatRenderer.java
index 8c172b6..0d7482e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatRenderer.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,12 +16,11 @@
 
 import com.google.gerrit.server.audit.AuditEvent;
 import com.google.gerrit.server.audit.SshAuditEvent;
+import java.util.Optional;
 
-public class AuditEventFormat implements AuditFormatter<SshAuditEvent> {
-  public static final Class<?> CLASS = AuditEvent.class;
+public interface AuditFormatRenderer {
 
-  @Override
-  public String format(SshAuditEvent result) {
-    return "";
-  }
+  String render(AuditEvent auditEvent);
+
+  Optional<String> headers();
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatTypes.java
similarity index 83%
rename from src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatter.java
rename to src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatTypes.java
index 67c2589..518cc84 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatTypes.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
 
 package com.googlesource.gerrit.plugins.auditsl4j;
 
-public interface AuditFormatter<T> {
-
-  String format(T result);
+public enum AuditFormatTypes {
+  CSV,
+  JSON;
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
similarity index 65%
copy from src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java
copy to src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
index 8c172b6..d33bd12 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -15,13 +15,16 @@
 package com.googlesource.gerrit.plugins.auditsl4j;
 
 import com.google.gerrit.server.audit.AuditEvent;
-import com.google.gerrit.server.audit.SshAuditEvent;
 
-public class AuditEventFormat implements AuditFormatter<SshAuditEvent> {
-  public static final Class<?> CLASS = AuditEvent.class;
+public class AuditRecord {
+  public final String type;
+  public final AuditEvent event;
 
-  @Override
-  public String format(SshAuditEvent result) {
-    return "";
+  public AuditRecord(AuditEvent event) {
+    super();
+
+    String eventClass = event.getClass().getName();
+    this.type = eventClass.substring(eventClass.lastIndexOf('.') + 1);
+    this.event = event;
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
new file mode 100644
index 0000000..50d4b1e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.common.collect.Multimap;
+import com.google.gerrit.server.audit.AuditEvent;
+import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
+import com.google.gerrit.server.audit.HttpAuditEvent;
+import com.google.gerrit.server.audit.RpcAuditEvent;
+import com.google.gerrit.server.audit.SshAuditEvent;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class AuditRendererToCsv implements AuditFormatRenderer {
+
+  private static final SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss.SSSS");
+
+  @SuppressWarnings("serial")
+  private static final Map<Class<?>, CsvFieldFormatter<?>> FIELD_CSV_FORMATTERS =
+      Collections.unmodifiableMap(
+          new HashMap<Class<?>, CsvFieldFormatter<? extends Object>>() {
+            {
+              put(HttpAuditEvent.class, new HttpAuditEventFormat());
+              put(ExtendedHttpAuditEvent.class, new HttpAuditEventFormat());
+              put(RpcAuditEvent.class, new RpcAuditEventFormat());
+              put(SshAuditEvent.class, new SshAuditEventFormat());
+              put(AuditEvent.class, new AuditEventFormat());
+            }
+          });
+
+  interface CsvFieldFormatter<T> {
+    String formatToCsv(T result);
+  }
+
+  static class RpcAuditEventFormat implements CsvFieldFormatter<RpcAuditEvent> {
+    @Override
+    public String formatToCsv(RpcAuditEvent result) {
+      return "RPC-" + result.httpMethod + ", Status:" + result.httpStatus;
+    }
+  }
+
+  static class HttpAuditEventFormat implements CsvFieldFormatter<HttpAuditEvent> {
+
+    @Override
+    public String formatToCsv(HttpAuditEvent result) {
+      return "HTTP-" + result.httpMethod + ", Status:" + result.httpStatus;
+    }
+  }
+
+  static class SshAuditEventFormat implements CsvFieldFormatter<SshAuditEvent> {
+    @Override
+    public String formatToCsv(SshAuditEvent result) {
+      return "SSH";
+    }
+  }
+
+  static class AuditEventFormat implements CsvFieldFormatter<SshAuditEvent> {
+
+    @Override
+    public String formatToCsv(SshAuditEvent result) {
+      return "";
+    }
+  }
+
+  @Override
+  public String render(AuditEvent auditEvent) {
+    return String.format(
+        "%1$s | %2$s | %3$s | %4$s | %5$s | %6$s | %7$s | %8$s | %9$s | %10$s",
+        auditEvent.uuid.uuid(),
+        getFormattedTS(auditEvent.when),
+        auditEvent.sessionId,
+        getFieldAsCsv(auditEvent.who),
+        getFieldAsCsv(auditEvent),
+        auditEvent.what,
+        getFormattedAuditList(auditEvent.params),
+        getFieldAsCsv(auditEvent.result),
+        getFormattedTS(auditEvent.timeAtStart),
+        auditEvent.elapsed);
+  }
+
+  @Override
+  public Optional<String> headers() {
+    return Optional.of(
+        "EventId | EventTS | SessionId | User | Protocol data | Action | Parameters | Result | StartTS | Elapsed");
+  }
+
+  private Object getFormattedAuditList(Multimap<String, ?> params) {
+    if (params == null || params.size() == 0) {
+      return "[]";
+    }
+
+    StringBuilder formattedOut = new StringBuilder("[");
+
+    Set<String> paramNames = new TreeSet<>(params.keySet());
+
+    int numParams = 0;
+    for (String paramName : paramNames) {
+      if (numParams++ > 0) {
+        formattedOut.append(",");
+      }
+      formattedOut.append(paramName);
+      formattedOut.append("=");
+      formattedOut.append(getFormattedAudit(params.get(paramName)));
+    }
+
+    formattedOut.append(']');
+
+    return formattedOut.toString();
+  }
+
+  private Object getFormattedAudit(Collection<? extends Object> values) {
+    StringBuilder out = new StringBuilder();
+    int numValues = 0;
+    for (Object object : values) {
+      if (numValues > 0) {
+        out.append(",");
+      }
+      out.append(getFieldAsCsv(object));
+      numValues++;
+    }
+
+    if (numValues > 1) {
+      return "[" + out.toString() + "]";
+    }
+    return out.toString();
+  }
+
+  public static <T> String getFieldAsCsv(T result) {
+    if (result == null) return "";
+
+    @SuppressWarnings("unchecked")
+    CsvFieldFormatter<T> fmt = (CsvFieldFormatter<T>) FIELD_CSV_FORMATTERS.get(result.getClass());
+    if (fmt == null) return result.toString();
+
+    return fmt.formatToCsv(result);
+  }
+
+  public static synchronized String getFormattedTS(long when) {
+    return dateFmt.format(new Date(when));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java
new file mode 100644
index 0000000..ba02a53
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.common.collect.ListMultimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.audit.AuditEvent;
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Optional;
+
+public class AuditRendererToJson implements AuditFormatRenderer {
+  private final ExclusionStrategy INCLUDE_ONLY_WHITELISTED =
+      new ExclusionStrategy() {
+        private final HashSet<Class<?>> WHITELIST_CLASSES =
+            new HashSet<>(
+                Arrays.asList(
+                    String.class,
+                    Object.class,
+                    CurrentUser.class,
+                    Long.class,
+                    Long.TYPE,
+                    Integer.class,
+                    Integer.TYPE,
+                    AccessPath.class,
+                    CurrentUser.PropertyKey.class,
+                    Account.Id.class,
+                    AuditRecord.class));
+        private final HashSet<String> BLACKLIST_FIELDS =
+            new HashSet<>(Arrays.asList("anonymousCowardName"));
+
+        @Override
+        public boolean shouldSkipField(FieldAttributes f) {
+          return BLACKLIST_FIELDS.contains(f.getName());
+        }
+
+        @Override
+        public boolean shouldSkipClass(Class<?> clazz) {
+          return !AuditEvent.class.isAssignableFrom(clazz)
+              && !CurrentUser.class.isAssignableFrom(clazz)
+              && !ListMultimap.class.isAssignableFrom(clazz)
+              && !AuditEvent.UUID.class.isAssignableFrom(clazz)
+              && !WHITELIST_CLASSES.contains(clazz);
+        }
+      };
+
+  private final Gson gson =
+      OutputFormat.JSON_COMPACT
+          .newGsonBuilder()
+          .setExclusionStrategies(INCLUDE_ONLY_WHITELISTED)
+          .create();
+
+  @Override
+  public String render(AuditEvent auditEvent) {
+    return gson.toJson(new AuditRecord(auditEvent));
+  }
+
+  @Override
+  public Optional<String> headers() {
+    return Optional.empty();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatter.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriter.java
similarity index 71%
copy from src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatter.java
copy to src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriter.java
index 67c2589..1da9b2a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriter.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,7 +14,11 @@
 
 package com.googlesource.gerrit.plugins.auditsl4j;
 
-public interface AuditFormatter<T> {
+import com.google.gerrit.server.audit.HttpAuditEvent;
+import com.google.inject.ImplementedBy;
 
-  String format(T result);
+@ImplementedBy(AuditWriterToLogger.class)
+public interface AuditWriter {
+
+  void write(String msg);
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToAsyncAppender.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToAsyncAppender.java
new file mode 100644
index 0000000..194af59
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToAsyncAppender.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.gerrit.server.util.SystemLog;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Singleton;
+import org.apache.log4j.AsyncAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.spi.LoggingEvent;
+
+@Singleton
+public class AuditWriterToAsyncAppender implements AuditWriter {
+  private final Logger log = Logger.getLogger(LoggerAudit.AUDIT_LOGGER_NAME);
+  private final AsyncAppender appender;
+
+  public AuditWriterToAsyncAppender(AuditConfig config, SystemLog systemLog) {
+    String logName = config.getLogName().get();
+    appender = systemLog.createAsyncAppender(logName, new PatternLayout());
+  }
+
+  @Override
+  public void write(String auditBody) {
+    appender.append(newLoggingEvent(auditBody));
+  }
+
+  private LoggingEvent newLoggingEvent(String auditBody) {
+    return new LoggingEvent( //
+        LoggerAudit.AUDIT_LOGGER_NAME,
+        log, // logger
+        TimeUtil.nowMs(), // when
+        Level.INFO, // level
+        auditBody, // message text
+        "HTTPD", // thread name
+        null, // exception information
+        null, // current NDC string
+        null, // caller location
+        null // MDC properties
+        );
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToLogger.java
similarity index 67%
copy from src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java
copy to src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToLogger.java
index 8c172b6..98f64e2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditEventFormat.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToLogger.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,14 +14,15 @@
 
 package com.googlesource.gerrit.plugins.auditsl4j;
 
-import com.google.gerrit.server.audit.AuditEvent;
 import com.google.gerrit.server.audit.SshAuditEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-public class AuditEventFormat implements AuditFormatter<SshAuditEvent> {
-  public static final Class<?> CLASS = AuditEvent.class;
+public class AuditWriterToLogger implements AuditWriter {
+  private static final Logger log = LoggerFactory.getLogger(LoggerAudit.AUDIT_LOGGER_NAME);
 
   @Override
-  public String format(SshAuditEvent result) {
-    return "";
+  public void write(String auditBody) {
+    log.info(auditBody);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/RpcAuditEventFormat.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToStringList.java
similarity index 64%
rename from src/main/java/com/googlesource/gerrit/plugins/auditsl4j/RpcAuditEventFormat.java
rename to src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToStringList.java
index a910cf4..e612792 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/RpcAuditEventFormat.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditWriterToStringList.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,12 +16,21 @@
 
 import com.google.gerrit.server.audit.HttpAuditEvent;
 import com.google.gerrit.server.audit.RpcAuditEvent;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
 
-public class RpcAuditEventFormat implements AuditFormatter<HttpAuditEvent> {
-  protected static final Class<?> CLASS = RpcAuditEvent.class;
+@Singleton
+public class AuditWriterToStringList implements AuditWriter {
+  public final List<String> strings = new ArrayList<>();
 
   @Override
-  public String format(HttpAuditEvent result) {
-    return "RPC-" + result.httpMethod + ", Status:" + result.httpStatus;
+  public void write(String msg) {
+    strings.add(msg);
+  }
+
+  @Override
+  public String toString() {
+    return strings.toString();
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/HttpAuditEventFormat.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/HttpAuditEventFormat.java
deleted file mode 100644
index 43b884a..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/HttpAuditEventFormat.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// 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.
-
-package com.googlesource.gerrit.plugins.auditsl4j;
-
-import com.google.gerrit.server.audit.HttpAuditEvent;
-
-public class HttpAuditEventFormat implements AuditFormatter<HttpAuditEvent> {
-  protected static final Class<?> CLASS = HttpAuditEvent.class;
-
-  @Override
-  public String format(HttpAuditEvent result) {
-    return "HTTP-" + result.httpMethod + ", Status:" + result.httpStatus;
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
index 7ce623b..97cee43 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
@@ -14,117 +14,29 @@
 
 package com.googlesource.gerrit.plugins.auditsl4j;
 
-import com.google.common.collect.Multimap;
-import com.google.gerrit.extensions.annotations.Listen;
 import com.google.gerrit.server.audit.AuditEvent;
 import com.google.gerrit.server.audit.AuditListener;
+import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.text.SimpleDateFormat;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-@Listen
 @Singleton
 public class LoggerAudit implements AuditListener {
-  private static final Logger log = LoggerFactory.getLogger(LoggerAudit.class);
-  private final SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss.SSSS");
+  private final AuditWriter auditWriter;
+  private final AuditFormatRenderer auditRenderer;
 
-  @SuppressWarnings("serial")
-  private static final Map<Class<?>, AuditFormatter<?>> AUDIT_FORMATTERS =
-      Collections.unmodifiableMap(
-          new HashMap<Class<?>, AuditFormatter<? extends Object>>() {
-            {
-              put(HttpAuditEventFormat.CLASS, new HttpAuditEventFormat());
-              put(RpcAuditEventFormat.CLASS, new RpcAuditEventFormat());
-              put(SshAuditEventFormat.CLASS, new SshAuditEventFormat());
-              put(AuditEventFormat.CLASS, new AuditEventFormat());
-            }
-          });
+  public static final String AUDIT_LOGGER_NAME = LoggerAudit.class.getName();
 
-  static {
-    log.info(
-        "EventId | EventTS | SessionId | User | Protocol data | Action | Parameters | Result | StartTS | Elapsed");
+  @Inject
+  LoggerAudit(AuditWriter auditWriter, AuditFormatRenderer auditRenderer) {
+    this.auditWriter = auditWriter;
+    this.auditRenderer = auditRenderer;
+
+    auditRenderer.headers().ifPresent(auditWriter::write);
   }
 
   @Override
-  public void onAuditableAction(AuditEvent action) {
-    log.info(getFormattedAudit(action));
-  }
-
-  private String getFormattedAudit(AuditEvent action) {
-    return String.format(
-        "%1$s | %2$s | %3$s | %4$s | %5$s | %6$s | %7$s | %8$s | %9$s | %10$s",
-        action.uuid.uuid(),
-        getFormattedTS(action.when),
-        action.sessionId,
-        getFormattedAuditSingle(action.who),
-        getFormattedAuditSingle(action),
-        action.what,
-        getFormattedAuditList(action.params),
-        getFormattedAuditSingle(action.result),
-        getFormattedTS(action.timeAtStart),
-        action.elapsed);
-  }
-
-  private Object getFormattedAuditList(Multimap<String, ?> params) {
-    if (params == null || params.size() == 0) {
-      return "[]";
-    }
-
-    StringBuilder formattedOut = new StringBuilder("[");
-
-    Set<String> paramNames = new TreeSet<>(params.keySet());
-
-    int numParams = 0;
-    for (String paramName : paramNames) {
-      if (numParams++ > 0) {
-        formattedOut.append(",");
-      }
-      formattedOut.append(paramName);
-      formattedOut.append("=");
-      formattedOut.append(getFormattedAudit(params.get(paramName)));
-    }
-
-    formattedOut.append(']');
-
-    return formattedOut.toString();
-  }
-
-  private Object getFormattedAudit(Collection<? extends Object> values) {
-    StringBuilder out = new StringBuilder();
-    int numValues = 0;
-    for (Object object : values) {
-      if (numValues > 0) {
-        out.append(",");
-      }
-      out.append(getFormattedAuditSingle(object));
-      numValues++;
-    }
-
-    if (numValues > 1) {
-      return "[" + out.toString() + "]";
-    }
-    return out.toString();
-  }
-
-  private <T> String getFormattedAuditSingle(T result) {
-    if (result == null) return "";
-
-    @SuppressWarnings("unchecked")
-    AuditFormatter<T> fmt = (AuditFormatter<T>) AUDIT_FORMATTERS.get(result.getClass());
-    if (fmt == null) return result.toString();
-
-    return fmt.format(result);
-  }
-
-  private synchronized String getFormattedTS(long when) {
-    return dateFmt.format(new Date(when));
+  public void onAuditableAction(AuditEvent auditEvent) {
+    String auditString = auditRenderer.render(auditEvent);
+    auditWriter.write(auditString);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/Module.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/Module.java
new file mode 100644
index 0000000..fc11a00
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/Module.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.audit.AuditListener;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+public class Module extends AbstractModule {
+  private final AuditConfig config;
+
+  @Inject
+  public Module(AuditConfig config) {
+    this.config = config;
+  }
+
+  @Override
+  protected void configure() {
+    DynamicSet.bind(binder(), AuditListener.class).to(LoggerAudit.class);
+
+    AuditFormatTypes rendererType = config.getFormat();
+    switch (rendererType) {
+      case CSV:
+        bind(AuditFormatRenderer.class).to(AuditRendererToCsv.class);
+        break;
+      case JSON:
+        bind(AuditFormatRenderer.class).to(AuditRendererToJson.class);
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported renderer '" + rendererType + "'");
+    }
+
+    if (config.getLogName().isPresent()) {
+      bind(AuditWriter.class).to(AuditWriterToAsyncAppender.class);
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/SshAuditEventFormat.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/SshAuditEventFormat.java
deleted file mode 100644
index 1ded66b..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/SshAuditEventFormat.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// 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.
-
-package com.googlesource.gerrit.plugins.auditsl4j;
-
-import com.google.gerrit.server.audit.SshAuditEvent;
-
-public class SshAuditEventFormat implements AuditFormatter<SshAuditEvent> {
-  protected static final Class<?> CLASS = SshAuditEvent.class;
-
-  @Override
-  public String format(SshAuditEvent result) {
-    return "SSH";
-  }
-}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
new file mode 100644
index 0000000..4ad33c2
--- /dev/null
+++ b/src/main/resources/Documentation/about.md
@@ -0,0 +1,4 @@
+This plugin can push the audit events coming from Gerrit into
+a SLF4J appender named com.googlesource.gerrit.plugins.auditsl4j.LoggerAudit.
+
+The audit event object can be saved either in CSV or JSON format.
\ No newline at end of file
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
new file mode 100644
index 0000000..a18a878
--- /dev/null
+++ b/src/main/resources/Documentation/config.md
@@ -0,0 +1,16 @@
+Audit Configuration
+===================
+
+File `gerrit.config`
+--------------------
+
+The audit configuration can be defined in the main gerrit.config
+in a specific section dedicated to the audit-sl4j plugin.
+
+gerrit.audit-sl4j.format
+:	Output format of the audit record. Can be set to either JSON
+    or CSV. By default, CSV.
+    
+gerrit.audit-sl4j.logName
+:	Write audit to a separate log name under Gerrit logs directory.
+    By default, audit records are put into the error_log.
\ No newline at end of file
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToCsvTest.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToCsvTest.java
new file mode 100644
index 0000000..de89e0d
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToCsvTest.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.audit.AuditListener;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import org.apache.http.client.fluent.Request;
+import org.junit.Test;
+
+@Sandboxed
+@TestPlugin(
+    name = "audit-sl4j",
+    sysModule = "com.googlesource.gerrit.plugins.auditsl4j.LoggerAuditToCsvTest$TestModule")
+public class LoggerAuditToCsvTest extends LightweightPluginDaemonTest implements WaitForCondition {
+
+  @Inject @CanonicalWebUrl private String webUrl;
+
+  public static class TestModule extends AbstractModule {
+
+    @Override
+    protected void configure() {
+      bind(AuditWriter.class).to(AuditWriterToStringList.class);
+      bind(AuditFormatRenderer.class).to(AuditRendererToCsv.class);
+      DynamicSet.bind(binder(), AuditListener.class).to(LoggerAudit.class);
+    }
+  }
+
+  @Test
+  public void testHttpCsvAudit() throws Exception {
+    AuditWriterToStringList auditStrings = getPluginInstance(AuditWriterToStringList.class);
+
+    Request.Get(webUrl + "config/server/version").execute().returnResponse();
+
+    assertThat(waitFor(() -> auditStrings.strings.size() == 2)).isTrue();
+    assertThat(auditStrings.strings.get(1)).contains(Version.getVersion());
+  }
+
+  private <T> T getPluginInstance(Class<T> clazz) {
+    return plugin.getSysInjector().getInstance(clazz);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToJsonTest.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToJsonTest.java
new file mode 100644
index 0000000..9d0e48c
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAuditToJsonTest.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.audit.AuditListener;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import org.apache.http.client.fluent.Request;
+import org.junit.Test;
+
+@Sandboxed
+@TestPlugin(
+    name = "audit-sl4j",
+    sysModule = "com.googlesource.gerrit.plugins.auditsl4j.LoggerAuditToJsonTest$TestModule")
+public class LoggerAuditToJsonTest extends LightweightPluginDaemonTest implements WaitForCondition {
+
+  @Inject @CanonicalWebUrl private String webUrl;
+
+  public static class TestModule extends AbstractModule {
+
+    @Override
+    protected void configure() {
+      bind(AuditWriter.class).to(AuditWriterToStringList.class);
+      bind(AuditFormatRenderer.class).to(AuditRendererToJson.class);
+      DynamicSet.bind(binder(), AuditListener.class).to(LoggerAudit.class);
+    }
+  }
+
+  @Test
+  public void testHttpJsonAudit() throws Exception {
+    AuditWriterToStringList auditStrings = getPluginInstance(AuditWriterToStringList.class);
+
+    Request.Get(webUrl + "config/server/version").execute().returnResponse();
+
+    assertThat(waitFor(() -> auditStrings.strings.size() == 1)).isTrue();
+
+    String auditJsonString = auditStrings.strings.get(0);
+    assertThat(auditJsonString).contains(Version.getVersion());
+    JsonObject auditJson = new Gson().fromJson(auditJsonString, JsonObject.class);
+    assertThat(auditJson).isNotNull();
+  }
+
+  private <T> T getPluginInstance(Class<T> clazz) {
+    return plugin.getSysInjector().getInstance(clazz);
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/WaitForCondition.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/WaitForCondition.java
new file mode 100644
index 0000000..5548391
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/WaitForCondition.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.common.base.Stopwatch;
+import java.time.Duration;
+import java.util.function.Supplier;
+
+public interface WaitForCondition {
+
+  public default Duration waitTimeout() {
+    return Duration.ofSeconds(5);
+  }
+
+  public default Duration waitInterval() {
+    return Duration.ofMillis(100);
+  }
+
+  public default boolean waitFor(Supplier<Boolean> condition) {
+    Stopwatch stopwatch = Stopwatch.createStarted();
+    try {
+      Duration maxWait = waitTimeout();
+      Duration sleep = waitInterval();
+      boolean conditionSucceeded = condition.get();
+      while (!conditionSucceeded && stopwatch.elapsed().compareTo(maxWait) < 0) {
+        try {
+          Thread.sleep(sleep.toMillis());
+        } catch (InterruptedException e) {
+        }
+        conditionSucceeded = condition.get();
+      }
+      return conditionSucceeded;
+    } finally {
+      stopwatch.stop();
+    }
+  }
+}