This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/karaf.git

commit 6ff0873b2129eb2db09bf53875dc64fb3d26de85
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Nov 9 18:12:20 2017 +0100

    [KARAF-5475] Create an audit bundle that logs to file / tcp / udp / jul
---
 .../resources/etc/org.ops4j.pax.logging.cfg        |  10 +-
 .../features/standard/src/main/feature/feature.xml |  51 ++
 audit/pom.xml                                      | 118 +++++
 .../java/org/apache/karaf/audit/Activator.java     | 489 +++++++++++++++++++
 .../main/java/org/apache/karaf/audit/Event.java    |  48 ++
 .../java/org/apache/karaf/audit/EventLayout.java   |  35 ++
 .../java/org/apache/karaf/audit/EventLogger.java   |  30 ++
 .../apache/karaf/audit/layout/AbstractLayout.java  | 198 ++++++++
 .../org/apache/karaf/audit/layout/GelfLayout.java  |  89 ++++
 .../apache/karaf/audit/layout/Rfc3164Layout.java   |  85 ++++
 .../apache/karaf/audit/layout/Rfc5424Layout.java   |  86 ++++
 .../apache/karaf/audit/layout/SimpleLayout.java    |  69 +++
 .../apache/karaf/audit/logger/FileEventLogger.java | 291 ++++++++++++
 .../apache/karaf/audit/logger/JulEventLogger.java  |  61 +++
 .../apache/karaf/audit/logger/TcpEventLogger.java  |  67 +++
 .../apache/karaf/audit/logger/UdpEventLogger.java  |  84 ++++
 .../java/org/apache/karaf/audit/util/Buffer.java   | 306 ++++++++++++
 .../apache/karaf/audit/util/FastDateFormat.java    | 175 +++++++
 .../org/apache/karaf/audit/util/NumberOutput.java  | 516 +++++++++++++++++++++
 .../test/java/org/apache/karaf/audit/MapEvent.java |  66 +++
 .../test/java/org/apache/karaf/audit/TestPerf.java | 150 ++++++
 .../apache/karaf/audit/logger/EventLoggerTest.java | 264 +++++++++++
 .../karaf/audit/util/FastDateFormatTest.java       |  42 ++
 pom.xml                                            |   1 +
 .../felix/eventadmin/impl/Configuration.java       |   5 +-
 .../shell/impl/console/ConsoleSessionImpl.java     |   4 +
 .../impl/console/osgi/EventAdminListener.java      |  33 +-
 27 files changed, 3351 insertions(+), 22 deletions(-)

diff --git 
a/assemblies/features/base/src/main/resources/resources/etc/org.ops4j.pax.logging.cfg
 
b/assemblies/features/base/src/main/resources/resources/etc/org.ops4j.pax.logging.cfg
index 42d6c80..6cd8240 100644
--- 
a/assemblies/features/base/src/main/resources/resources/etc/org.ops4j.pax.logging.cfg
+++ 
b/assemblies/features/base/src/main/resources/resources/etc/org.ops4j.pax.logging.cfg
@@ -48,8 +48,8 @@ log4j2.logger.spifly.name = org.apache.aries.spifly
 log4j2.logger.spifly.level = WARN
 
 # Security audit logger
-log4j2.logger.audit.name = org.apache.karaf.jaas.modules.audit
-log4j2.logger.audit.level = INFO
+log4j2.logger.audit.name = audit
+log4j2.logger.audit.level = TRACE
 log4j2.logger.audit.additivity = false
 log4j2.logger.audit.appenderRef.AuditRollingFile.ref = AuditRollingFile
 
@@ -78,11 +78,11 @@ log4j2.appender.rolling.policies.size.size = 16MB
 # Audit file appender
 log4j2.appender.audit.type = RollingRandomAccessFile
 log4j2.appender.audit.name = AuditRollingFile
-log4j2.appender.audit.fileName = ${karaf.data}/security/audit.log
-log4j2.appender.audit.filePattern = ${karaf.data}/security/audit.log.%i
+log4j2.appender.audit.fileName = ${karaf.data}/log/security.log
+log4j2.appender.audit.filePattern = ${karaf.data}/log/security-%i.log
 log4j2.appender.audit.append = true
 log4j2.appender.audit.layout.type = PatternLayout
-log4j2.appender.audit.layout.pattern = ${log4j2.pattern}
+log4j2.appender.audit.layout.pattern = %m%n
 log4j2.appender.audit.policies.type = Policies
 log4j2.appender.audit.policies.size.type = SizeBasedTriggeringPolicy
 log4j2.appender.audit.policies.size.size = 8MB
diff --git a/assemblies/features/standard/src/main/feature/feature.xml 
b/assemblies/features/standard/src/main/feature/feature.xml
index c6b770a..d18fc56 100644
--- a/assemblies/features/standard/src/main/feature/feature.xml
+++ b/assemblies/features/standard/src/main/feature/feature.xml
@@ -1018,6 +1018,57 @@
         </config>
     </feature>
 
+    <feature name="audit-log" description="Security audit logging" 
version="${project.version}">
+        <feature>eventadmin</feature>
+        <bundle 
start-level="20">mvn:org.apache.karaf.audit/org.apache.karaf.audit.core/${project.version}</bundle>
+        <config name="org.apache.karaf.audit">
+            # Security audit configuration
+            # Only the above 4 loggers are supported
+            # Supported layouts include: simple, gelf, rfc3164, rfc5424
+
+            # Queue type
+            queue.class = java.util.concurrent.ArrayBlockingQueue
+            # Queue size
+            queue.size = 256
+            # Idle timeout
+            runner.idle-timeout = 60000
+            # Flush timeout
+            runner.flush-timeout = 100
+            # Event filter
+            # filter = (!(type=log))
+
+            # File logger
+            file.enabled = true
+            file.target = ${karaf.data}/log/audit.txt
+            file.encoding = UTF-8
+            file.layout.type = simple
+            # rotating policy: can be 'daily', 'size([0-9]+(kb|mb|gb)?\)'
+            file.policy = daily
+            file.files = 32
+            file.compress = false
+
+            # Tcp logger
+            # tcp.enabled = true
+            # tcp.host = localhost
+            # tcp.port = 8125
+            # tcp.encoding = UTF-8
+            # tcp.layout.type = gelf
+
+            # Udp logger
+            # udp.enabled = true
+            # udp.host = localhost
+            # udp.port = 514
+            # udp.encoding = UTF-8
+            # udp.layout.type = rfc3164
+
+            # JUL logger
+            jul.enabled = false
+            jul.logger = audit
+            jul.level = info
+            jul.layout.type = simple
+        </config>
+    </feature>
+
     <feature name="standard" description="Wrap feature describing all features 
part of a standard distribution" version="${project.version}">
         <feature>wrap</feature>
         <feature>aries-blueprint</feature>
diff --git a/audit/pom.xml b/audit/pom.xml
new file mode 100644
index 0000000..5581e06
--- /dev/null
+++ b/audit/pom.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+    <!--
+
+        Licensed to the Apache Software Foundation (ASF) under one or more
+        contributor license agreements.  See the NOTICE file distributed with
+        this work for additional information regarding copyright ownership.
+        The ASF licenses this file to You 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.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.karaf</groupId>
+        <artifactId>karaf</artifactId>
+        <version>4.2.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <groupId>org.apache.karaf.audit</groupId>
+    <artifactId>org.apache.karaf.audit.core</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Karaf :: Audit :: Core</name>
+    <description>This bundle provides Audit support for Karaf</description>
+
+    <properties>
+        
<appendedResourcesDirectory>${basedir}/../etc/appended-resources</appendedResourcesDirectory>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.services</groupId>
+            <artifactId>org.apache.karaf.services.eventadmin</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf</groupId>
+            <artifactId>org.apache.karaf.util</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.conversantmedia</groupId>
+            <artifactId>disruptor</artifactId>
+            <version>1.2.11</version>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-jdk14</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>${project.basedir}/src/main/resources</directory>
+                <includes>
+                    <include>**/*</include>
+                </includes>
+            </resource>
+            <resource>
+                <directory>${project.basedir}/src/main/resources</directory>
+                <filtering>true</filtering>
+                <includes>
+                    <include>**/*.info</include>
+                </includes>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.karaf.tooling</groupId>
+                <artifactId>karaf-services-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                        </Export-Package>
+                        <Import-Package>
+                            *
+                        </Import-Package>
+                        <Private-Package>
+                            org.apache.karaf.audit*
+                        </Private-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/audit/src/main/java/org/apache/karaf/audit/Activator.java 
b/audit/src/main/java/org/apache/karaf/audit/Activator.java
new file mode 100644
index 0000000..1066aed
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/Activator.java
@@ -0,0 +1,489 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit;
+
+import com.conversantmedia.util.concurrent.DisruptorBlockingQueue;
+import org.apache.karaf.audit.layout.GelfLayout;
+import org.apache.karaf.audit.layout.Rfc3164Layout;
+import org.apache.karaf.audit.layout.Rfc5424Layout;
+import org.apache.karaf.audit.layout.SimpleLayout;
+import org.apache.karaf.audit.logger.FileEventLogger;
+import org.apache.karaf.audit.logger.JulEventLogger;
+import org.apache.karaf.audit.logger.UdpEventLogger;
+import org.apache.karaf.util.tracker.BaseActivator;
+import org.apache.karaf.util.tracker.annotation.Managed;
+import org.apache.karaf.util.tracker.annotation.RequireService;
+import org.apache.karaf.util.tracker.annotation.Services;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
+
+import javax.security.auth.Subject;
+import java.io.IOException;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+@Services(requires = @RequireService(EventAdmin.class))
+@Managed("org.apache.karaf.audit")
+public class Activator extends BaseActivator implements ManagedService {
+
+    public static final String FILTER = "filter";
+    public static final String QUEUE_TYPE = "queue.type";
+    public static final String QUEUE_SIZE = "queue.size";
+    public static final String RUNNER_IDLE_TIMEOUT = "runner.idle-timeout";
+    public static final String RUNNER_FLUSH_TIMEOUT = "runner.flush-timeout";
+    public static final String FILE_PREFIX = "file.";
+    public static final String FILE_LAYOUT = FILE_PREFIX + "layout";
+    public static final String FILE_ENABLED = FILE_PREFIX + "enabled";
+    public static final String FILE_TARGET = FILE_PREFIX + "target";
+    public static final String FILE_ENCODING = FILE_PREFIX + "encoding";
+    public static final String FILE_POLICY = FILE_PREFIX + "policy";
+    public static final String FILE_FILES = FILE_PREFIX + "files";
+    public static final String FILE_COMPRESS = FILE_PREFIX + "compress";
+    public static final String UDP_PREFIX = "udp.";
+    public static final String UDP_LAYOUT = UDP_PREFIX + "layout";
+    public static final String UDP_ENABLED = UDP_PREFIX + "enabled";
+    public static final String UDP_HOST = UDP_PREFIX + "host";
+    public static final String UDP_PORT = UDP_PREFIX + "port";
+    public static final String UDP_ENCODING = UDP_PREFIX + "encoding";
+    public static final String TCP_PREFIX = "tcp.";
+    public static final String TCP_LAYOUT = TCP_PREFIX + "layout";
+    public static final String TCP_ENABLED = TCP_PREFIX + "enabled";
+    public static final String TCP_HOST = TCP_PREFIX + "host";
+    public static final String TCP_PORT = TCP_PREFIX + "port";
+    public static final String TCP_ENCODING = TCP_PREFIX + "encoding";
+    public static final String JUL_PREFIX = "jul.";
+    public static final String JUL_LAYOUT = JUL_PREFIX + "layout";
+    public static final String JUL_ENABLED = JUL_PREFIX + "enabled";
+    public static final String JUL_LOGGER = JUL_PREFIX + "logger";
+    public static final String JUL_LEVEL = JUL_PREFIX + "level";
+    public static final String TOPICS = "topics";
+
+    private static final EventImpl STOP_EVENT = new EventImpl(new 
Event("stop", Collections.emptyMap()));
+
+
+    private BlockingQueue<EventImpl> queue;
+    private volatile Thread runner;
+    private List<EventLogger> eventLoggers;
+    private Filter filter;
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        queue = createQueue();
+        eventLoggers = createLoggers();
+        filter = createFilter();
+        final Dictionary<String, Object> props = new Hashtable<>();
+        props.put(EventConstants.EVENT_TOPIC, getTopics());
+        register(EventHandler.class, this::handleEvent, props);
+        if (!queue.isEmpty()) {
+            startRunner();
+        }
+    }
+
+    private String[] getTopics() {
+        return getString(TOPICS, "*").split("\\s*,\\s*");
+    }
+
+    private Filter createFilter() throws InvalidSyntaxException {
+        String str = getString(FILTER, null);
+        return str != null ? FrameworkUtil.createFilter(str) : null;
+    }
+
+    @SuppressWarnings("unchecked")
+    private BlockingQueue<EventImpl> createQueue() throws Exception {
+        String type = getString(QUEUE_TYPE, null);
+        int size = getInt(QUEUE_SIZE, 1024);
+        if ("ArrayBlockingQueue".equals(type)) {
+            return new ArrayBlockingQueue<>(size);
+        } else if ("DisruptorBlockingQueue".equals(type)) {
+            return new DisruptorBlockingQueue(size);
+        } else if (type != null) {
+            logger.warn("Unknown queue type: " + type + "");
+        }
+        try {
+            return new DisruptorBlockingQueue(size);
+        } catch (NoClassDefFoundError t) {
+            return new ArrayBlockingQueue<>(size);
+        }
+    }
+
+    private List<EventLogger> createLoggers() throws Exception {
+        try {
+            List<EventLogger> loggers = new ArrayList<>();
+            if (getBoolean(FILE_ENABLED, true)) {
+                String path = getString(FILE_TARGET, 
System.getProperty("karaf.data") + "/log/audit.txt");
+                String encoding = getString(FILE_ENCODING, "UTF-8");
+                String policy = getString(FILE_POLICY, "size(8mb)");
+                int files = getInt(FILE_FILES, 32);
+                boolean compress = getBoolean(FILE_COMPRESS, true);
+                EventLayout layout = createLayout(getString(FILE_LAYOUT, 
FILE_LAYOUT));
+                loggers.add(new FileEventLogger(path, encoding, policy, files, 
compress, this, layout));
+            }
+            if (getBoolean(UDP_ENABLED, false)) {
+                String host = getString(UDP_HOST, "localhost");
+                int port = getInt(UDP_PORT, 514);
+                String encoding = getString(UDP_ENCODING, "UTF-8");
+                EventLayout layout = createLayout(getString(UDP_LAYOUT, 
UDP_LAYOUT));
+                loggers.add(new UdpEventLogger(host, port, encoding, layout));
+            }
+            if (getBoolean(TCP_ENABLED, false)) {
+                String host = getString(TCP_HOST, "localhost");
+                int port = getInt(TCP_PORT, 0);
+                String encoding = getString(TCP_ENCODING, "UTF-8");
+                EventLayout layout = createLayout(getString(TCP_LAYOUT, 
TCP_LAYOUT));
+                loggers.add(new UdpEventLogger(host, port, encoding, layout));
+            }
+            if (getBoolean(JUL_ENABLED, false)) {
+                String logger = getString(Activator.JUL_LOGGER, "audit");
+                String level = getString(Activator.JUL_LEVEL, "info");
+                EventLayout layout = createLayout(getString(JUL_LAYOUT, 
JUL_LAYOUT));
+                loggers.add(new JulEventLogger(logger, level, layout));
+            }
+            return loggers;
+        } catch (IOException e) {
+            throw new Exception("Error creating audit logger", e);
+        }
+    }
+
+    private EventLayout createLayout(String prefix) {
+        String type = getString(prefix + ".type", "simple");
+        switch (type) {
+            case "simple":
+                return new SimpleLayout();
+            case "rfc3164":
+                return new Rfc3164Layout(getInt(prefix + ".facility", 16),
+                        getInt(prefix + ".priority", 5),
+                        getInt(prefix + ".enterprise", 
Rfc5424Layout.DEFAULT_ENTERPRISE_NUMBER));
+            case "rfc5424":
+                return new Rfc5424Layout(getInt(prefix + ".facility", 16),
+                                         getInt(prefix + ".priority", 5),
+                                         getInt(prefix + ".enterprise", 
Rfc5424Layout.DEFAULT_ENTERPRISE_NUMBER));
+            case "gelf":
+                return new GelfLayout();
+            default:
+                logger.warn("Unknown layout: " + type + ". Using a simple 
layout.");
+                return new SimpleLayout();
+        }
+    }
+
+    @Override
+    protected void doStop() {
+        Thread runner = this.runner;
+        if (runner != null && runner.isAlive()) {
+            try {
+                queue.add(STOP_EVENT);
+                runner.join(5000);
+                if (runner.isAlive()) {
+                    runner.interrupt();
+                }
+            } catch (InterruptedException e) {
+                logger.debug("Error waiting for audit runner buffer stop");
+            }
+        }
+        List<EventLogger> eventLoggers = this.eventLoggers;
+        if (eventLoggers != null) {
+            for (EventLogger eventLogger : eventLoggers) {
+                try {
+                    eventLogger.close();
+                } catch (IOException e) {
+                    logger.debug("Error closing audit logger", e);
+                }
+            }
+            this.eventLoggers = null;
+        }
+        super.doStop();
+    }
+
+    private void handleEvent(Event event) {
+        try {
+            EventImpl ev = new EventImpl(event);
+            if (filter == null || filter.matches(ev.getFilterMap())) {
+                queue.put(new EventImpl(event));
+                startRunner();
+            }
+        } catch (InterruptedException e) {
+            logger.debug("Interrupted while putting event in queue", e);
+        }
+    }
+
+    private void startRunner() {
+        if (eventLoggers != null && !eventLoggers.isEmpty() && runner == null) 
{
+            synchronized (this) {
+                if (runner == null) {
+                    runner = new Thread(this::consume, "audit-logger");
+                    runner.start();
+                }
+            }
+        }
+    }
+
+    private void consume() {
+        long maxIdle = getLong(RUNNER_IDLE_TIMEOUT, 
TimeUnit.MINUTES.toMillis(1));
+        long flushDelay = getLong(RUNNER_FLUSH_TIMEOUT, 
TimeUnit.MILLISECONDS.toMillis(100));
+        try {
+            List<EventLogger> eventLoggers = this.eventLoggers;
+            BlockingQueue<EventImpl> queue = this.queue;
+            EventImpl event;
+            while ((event = queue.poll(maxIdle, TimeUnit.MILLISECONDS)) != 
null) {
+                if (event == STOP_EVENT) {
+                    return;
+                }
+                for (EventLogger eventLogger : eventLoggers) {
+                    eventLogger.write(event);
+                }
+                if (flushDelay > 0) {
+                    while ((event = queue.poll(flushDelay, 
TimeUnit.MILLISECONDS)) != null) {
+                        if (event == STOP_EVENT) {
+                            return;
+                        }
+                        for (EventLogger eventLogger : eventLoggers) {
+                            eventLogger.write(event);
+                        }
+                    }
+                }
+                for (EventLogger eventLogger : eventLoggers) {
+                    eventLogger.flush();
+                }
+            }
+        } catch (Throwable e) {
+            logger.warn("Error writing audit log", e);
+        } finally {
+            runner = null;
+        }
+    }
+
+    static class EventImpl implements org.apache.karaf.audit.Event {
+        private final Event event;
+        private final long timestamp;
+        private final String type;
+        private final String subtype;
+
+        EventImpl(Event event) {
+            this.event = event;
+            this.timestamp = _timestamp();
+            this.type = _type();
+            this.subtype = _subtype();
+        }
+
+        @Override
+        public long timestamp() {
+            return timestamp;
+        }
+
+        private long _timestamp() {
+            Long l = (Long) event.getProperty("timestamp");
+            return l != null ? l : System.currentTimeMillis();
+        }
+
+        @Override
+        public Subject subject() {
+            return (Subject) event.getProperty("subject");
+        }
+
+        @Override
+        public String type() {
+            return type;
+        }
+
+        private String _type() {
+            switch (event.getTopic()) {
+                case "org/apache/karaf/shell/console/EXECUTED":
+                    return TYPE_SHELL;
+                case "org/osgi/service/log/LogEntry/LOG_ERROR":
+                case "org/osgi/service/log/LogEntry/LOG_WARNING":
+                case "org/osgi/service/log/LogEntry/LOG_INFO":
+                case "org/osgi/service/log/LogEntry/LOG_DEBUG":
+                case "org/osgi/service/log/LogEntry/LOG_OTHER":
+                    return TYPE_LOG;
+                case "org/osgi/framework/ServiceEvent/REGISTERED":
+                case "org/osgi/framework/ServiceEvent/MODIFIED":
+                case "org/osgi/framework/ServiceEvent/UNREGISTERING":
+                    return TYPE_SERVICE;
+                case "org/osgi/framework/BundleEvent/INSTALLED":
+                case "org/osgi/framework/BundleEvent/STARTED":
+                case "org/osgi/framework/BundleEvent/STOPPED":
+                case "org/osgi/framework/BundleEvent/UPDATED":
+                case "org/osgi/framework/BundleEvent/UNINSTALLED":
+                case "org/osgi/framework/BundleEvent/RESOLVED":
+                case "org/osgi/framework/BundleEvent/UNRESOLVED":
+                case "org/osgi/framework/BundleEvent/STARTING":
+                case "org/osgi/framework/BundleEvent/STOPPING":
+                    return TYPE_BUNDLE;
+                case "org/apache/karaf/login/ATTEMPT":
+                case "org/apache/karaf/login/SUCCESS":
+                case "org/apache/karaf/login/FAILURE":
+                case "org/apache/karaf/login/LOGOUT":
+                    return TYPE_LOGIN;
+                case "javax/management/MBeanServer/CREATEMBEAN":
+                case "javax/management/MBeanServer/REGISTERMBEAN":
+                case "javax/management/MBeanServer/UNREGISTERMBEAN":
+                case "javax/management/MBeanServer/GETOBJECTINSTANCE":
+                case "javax/management/MBeanServer/QUERYMBEANS":
+                case "javax/management/MBeanServer/ISREGISTERED":
+                case "javax/management/MBeanServer/GETMBEANCOUNT":
+                case "javax/management/MBeanServer/GETATTRIBUTE":
+                case "javax/management/MBeanServer/GETATTRIBUTES":
+                case "javax/management/MBeanServer/SETATTRIBUTE":
+                case "javax/management/MBeanServer/SETATTRIBUTES":
+                case "javax/management/MBeanServer/INVOKE":
+                case "javax/management/MBeanServer/GETDEFAULTDOMAIN":
+                case "javax/management/MBeanServer/GETDOMAINS":
+                case "javax/management/MBeanServer/ADDNOTIFICATIONLISTENER":
+                case "javax/management/MBeanServer/GETMBEANINFO":
+                case "javax/management/MBeanServer/ISINSTANCEOF":
+                case "javax/management/MBeanServer/INSTANTIATE":
+                case "javax/management/MBeanServer/DESERIALIZE":
+                case "javax/management/MBeanServer/GETCLASSLOADERFOR":
+                case "javax/management/MBeanServer/GETCLASSLOADER":
+                    return TYPE_JMX;
+                case "org/osgi/framework/FrameworkEvent/STARTED":
+                case "org/osgi/framework/FrameworkEvent/ERROR":
+                case "org/osgi/framework/FrameworkEvent/PACKAGES_REFRESHED":
+                case "org/osgi/framework/FrameworkEvent/STARTLEVEL_CHANGED":
+                case "org/osgi/framework/FrameworkEvent/WARNING":
+                case "org/osgi/framework/FrameworkEvent/INFO":
+                case "org/osgi/framework/FrameworkEvent/STOPPED":
+                case "org/osgi/framework/FrameworkEvent/STOPPED_UPDATE":
+                case 
"org/osgi/framework/FrameworkEvent/STOPPED_BOOTCLASSPATH_MODIFIED":
+                case "org/osgi/framework/FrameworkEvent/WAIT_TIMEDOUT":
+                    return TYPE_FRAMEWORK;
+                case "org/osgi/service/web/DEPLOYING":
+                case "org/osgi/service/web/DEPLOYED":
+                case "org/osgi/service/web/UNDEPLOYING":
+                case "org/osgi/service/web/UNDEPLOYED":
+                    return TYPE_WEB;
+                case "org/apache/karaf/features/repositories/ADDED":
+                case "org/apache/karaf/features/repositories/REMOVED":
+                    return TYPE_REPOSITORIES;
+                case "org/apache/karaf/features/features/INSTALLED":
+                case "org/apache/karaf/features/features/UNINSTALLED":
+                    return TYPE_FEATURES;
+                case "org/osgi/service/blueprint/container/CREATING":
+                case "org/osgi/service/blueprint/container/CREATED":
+                case "org/osgi/service/blueprint/container/DESTROYING":
+                case "org/osgi/service/blueprint/container/DESTROYED":
+                case "org/osgi/service/blueprint/container/FAILURE":
+                case "org/osgi/service/blueprint/container/GRACE_PERIOD":
+                case "org/osgi/service/blueprint/container/WAITING":
+                    return TYPE_BLUEPRINT;
+                default:
+                    return TYPE_UNKNOWN;
+            }
+        }
+
+        @Override
+        public String subtype() {
+            return subtype;
+        }
+
+        private String _subtype() {
+            String topic = event.getTopic();
+            return topic.substring(topic.lastIndexOf('/') + 
1).toLowerCase(Locale.ENGLISH);
+        }
+
+        @Override
+        public Iterable<String> keys() {
+            String[] keys = event.getPropertyNames();
+            Arrays.sort(keys);
+            return () -> new Iterator<String>() {
+                String next;
+                int index = -1;
+                @Override
+                public boolean hasNext() {
+                    if (next != null) {
+                        return true;
+                    }
+                    while (++index < keys.length) {
+                        switch (keys[index]) {
+                            case "timestamp":
+                            case "event.topics":
+                            case "subject":
+                            case "type":
+                            case "subtype":
+                                break;
+                            default:
+                                next = keys[index];
+                                return true;
+                        }
+                    }
+                    return false;
+                }
+                @Override
+                public String next() {
+                    if (!hasNext()) {
+                        throw new NoSuchElementException();
+                    }
+                    String str = next;
+                    next = null;
+                    return str;
+                }
+            };
+        }
+
+        @Override
+        public Object getProperty(String key) {
+            return event.getProperty(key);
+        }
+
+        Map<String, Object> getFilterMap() {
+            return new AbstractMap<String, Object>() {
+                @Override
+                public Set<Entry<String, Object>> entrySet() {
+                    throw new UnsupportedOperationException();
+                }
+
+                @Override
+                public Object get(Object key) {
+                    String s = key.toString();
+                    switch (s) {
+                        case "timestamp":
+                            return timestamp();
+                        case "type":
+                            return type();
+                        case "subtype":
+                            return subtype();
+                        case "subject":
+                            return subject();
+                        default:
+                            return event.getProperty(s);
+                    }
+                }
+            };
+        }
+
+    }
+
+}
diff --git a/audit/src/main/java/org/apache/karaf/audit/Event.java 
b/audit/src/main/java/org/apache/karaf/audit/Event.java
new file mode 100644
index 0000000..d49d083
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/Event.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit;
+
+import javax.security.auth.Subject;
+
+public interface Event {
+    
+    String TYPE_SHELL = "shell";
+    String TYPE_LOG = "log";
+    String TYPE_SERVICE = "service";
+    String TYPE_BUNDLE = "bundle";
+    String TYPE_LOGIN = "login";
+    String TYPE_JMX = "jmx";
+    String TYPE_FRAMEWORK= "framework";
+    String TYPE_WEB = "web";
+    String TYPE_REPOSITORIES = "repositories";
+    String TYPE_FEATURES = "features";
+    String TYPE_BLUEPRINT = "blueprint";
+    String TYPE_UNKNOWN = "unknown";
+    
+    long timestamp();
+    
+    Subject subject();
+    
+    String type();
+    
+    String subtype();
+    
+    Iterable<String> keys();
+    
+    Object getProperty(String key);
+
+}
diff --git a/audit/src/main/java/org/apache/karaf/audit/EventLayout.java 
b/audit/src/main/java/org/apache/karaf/audit/EventLayout.java
new file mode 100644
index 0000000..e2d5ea5
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/EventLayout.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit;
+
+import java.io.IOException;
+import java.nio.CharBuffer;
+
+public interface EventLayout {
+
+    /**
+     * Format the log event directly into the given <code>Appendable</code>.
+     */
+    void format(Event event, Appendable to) throws IOException;
+
+    /**
+     * Format the log event and return a CharBuffer.  The buffer is only valid
+     * until the next call to {@link #format(Event)} or {@link #format(Event, 
Appendable)}.
+     */
+    CharBuffer format(Event event) throws IOException;
+
+}
diff --git a/audit/src/main/java/org/apache/karaf/audit/EventLogger.java 
b/audit/src/main/java/org/apache/karaf/audit/EventLogger.java
new file mode 100644
index 0000000..159cc7e
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/EventLogger.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit;
+
+import java.io.Closeable;
+import java.io.Flushable;
+import java.io.IOException;
+
+public interface EventLogger extends Flushable, Closeable {
+
+    /**
+     * Write the event.
+     */
+    void write(Event event) throws IOException;
+
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/layout/AbstractLayout.java 
b/audit/src/main/java/org/apache/karaf/audit/layout/AbstractLayout.java
new file mode 100644
index 0000000..990408c
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/layout/AbstractLayout.java
@@ -0,0 +1,198 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.layout;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.EventLayout;
+import org.apache.karaf.audit.util.Buffer;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.CharBuffer;
+import java.util.Enumeration;
+
+public abstract class AbstractLayout implements EventLayout {
+
+    protected final String hostName;
+    protected final String appName;
+    protected final String procId;
+    
+    protected final Buffer buffer;
+
+    public AbstractLayout(Buffer buffer) {
+        this.hostName = hostname();
+        this.appName = System.getProperty("karaf.name", "-");
+        this.procId = procId();
+        this.buffer = buffer;
+    }
+
+    @Override
+    public void format(Event event, Appendable to) throws IOException {
+        doFormat(event);
+        buffer.writeTo(to);
+    }
+
+    @Override
+    public CharBuffer format(Event event) throws IOException {
+        doFormat(event);
+        return CharBuffer.wrap(buffer.buffer(), 0, buffer.position());
+    }
+
+    private void doFormat(Event event) throws IOException {
+        buffer.clear();
+        header(event);
+        message(event);
+        footer(event);
+    }
+
+    protected abstract void header(Event event) throws IOException;
+
+    protected abstract void footer(Event event) throws IOException;
+
+    protected void message(Event event) throws IOException {
+        append("subject", event.subject());
+        append("type", event.type());
+        append("subtype", event.subtype());
+        String message = null;
+        switch (event.type()) {
+            case Event.TYPE_SHELL: {
+                append(event, "script");
+                append(event, "command");
+                append(event, "exception");
+                break;
+            }
+            case Event.TYPE_LOGIN: {
+                append(event, "username");
+                break;
+            }
+            case Event.TYPE_JMX: {
+                append(event, "method");
+                append(event, "signature");
+                append(event, "params");
+                append(event, "result");
+                append(event, "exception");
+                break;
+            }
+            case Event.TYPE_LOG: {
+                Bundle bundle = (Bundle) event.getProperty("bundle");
+                if (bundle != null) {
+                    append("bundle.id", bundle.getBundleId());
+                    append("bundle.symbolicname", bundle.getSymbolicName());
+                    append("bundle.version", bundle.getVersion());
+                }
+                append(event, "message");
+                append(event, "exception");
+                break;
+            }
+            case Event.TYPE_BUNDLE: {
+                Bundle bundle = (Bundle) event.getProperty("bundle");
+                append("bundle.id", bundle.getBundleId());
+                append("bundle.symbolicname", bundle.getSymbolicName());
+                append("bundle.version", bundle.getVersion());
+                break;
+            }
+            case Event.TYPE_SERVICE: {
+                ServiceEvent se = (ServiceEvent) event.getProperty("event");
+                append("service.bundleid", 
se.getServiceReference().getProperty(Constants.SERVICE_BUNDLEID));
+                append("service.id", 
se.getServiceReference().getProperty(Constants.SERVICE_ID));
+                append("objectClass", 
se.getServiceReference().getProperty(Constants.OBJECTCLASS));
+                break;
+            }
+            case Event.TYPE_WEB: {
+                append(event, "servlet.servlet");
+                append(event, "servlet.alias");
+                break;
+            }
+            case Event.TYPE_REPOSITORIES: {
+                append(event, "uri");
+                break;
+            }
+            case Event.TYPE_FEATURES: {
+                append(event, "name");
+                append(event, "version");
+                break;
+            }
+            case Event.TYPE_BLUEPRINT: {
+                append(event, "bundle.id");
+                append(event, "bundle.symbolicname");
+                append(event, "bundle.version");
+                break;
+            }
+            default: {
+                for (String key : event.keys()) {
+                    append(event, key);
+                }
+                break;
+            }
+        }
+    }
+
+    private void append(Event event, String key) throws IOException {
+        append(key, event.getProperty(key));
+    }
+
+    protected abstract void append(String key, Object val) throws IOException;
+
+    private static String hostname() {
+        try {
+            final InetAddress addr = InetAddress.getLocalHost();
+            return addr.getHostName();
+        } catch (final UnknownHostException uhe) {
+            try {
+                final Enumeration<NetworkInterface> interfaces = 
NetworkInterface.getNetworkInterfaces();
+                while (interfaces.hasMoreElements()) {
+                    final NetworkInterface nic = interfaces.nextElement();
+                    final Enumeration<InetAddress> addresses = 
nic.getInetAddresses();
+                    while (addresses.hasMoreElements()) {
+                        final InetAddress address = addresses.nextElement();
+                        if (!address.isLoopbackAddress()) {
+                            final String hostname = address.getHostName();
+                            if (hostname != null) {
+                                return hostname;
+                            }
+                        }
+                    }
+                }
+            } catch (final SocketException se) {
+                // Ignore exception.
+            }
+            return "-";
+        }
+    }
+
+    private static String procId() {
+        try {
+            return 
ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; // likely works 
on most platforms
+        } catch (final Exception ex) {
+            try {
+                return new File("/proc/self").getCanonicalFile().getName(); // 
try a Linux-specific way
+            } catch (final IOException ignoredUseDefault) {
+                // Ignore exception.
+            }
+        }
+        return "-";
+    }
+
+}
diff --git a/audit/src/main/java/org/apache/karaf/audit/layout/GelfLayout.java 
b/audit/src/main/java/org/apache/karaf/audit/layout/GelfLayout.java
new file mode 100644
index 0000000..bb760e5
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/layout/GelfLayout.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.layout;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.util.Buffer;
+
+import java.io.IOException;
+
+public class GelfLayout extends AbstractLayout {
+
+    public GelfLayout() {
+        super(new Buffer(Buffer.Format.Json));
+    }
+
+    @Override
+    protected void header(Event event) throws IOException {
+        buffer.append('{');
+        append("version", "1.1", false);
+        append("host", hostName, false);
+        datetime(event.timestamp());
+        append("short_message", event.type() + "." + event.subtype(), false);
+    }
+
+    private void datetime(long timestamp) throws IOException {
+        buffer.append(" timestamp=");
+        long secs = timestamp / 1000;
+        int ms = (int)(timestamp - secs * 1000);
+        buffer.format(secs);
+        buffer.append('.');
+        int temp = ms / 100;
+        buffer.append((char) (temp + '0'));
+        ms -= 100 * temp;
+        temp = ms / 10;
+        buffer.append((char) (temp + '0'));
+        ms -= 10 * temp;
+        buffer.append((char) (ms + '0'));
+    }
+
+    @Override
+    protected void footer(Event event) throws IOException {
+        buffer.append(' ');
+        buffer.append('}');
+    }
+
+    @Override
+    protected void append(String key, Object val) throws IOException {
+        append(key, val, true);
+    }
+
+    protected void append(String key, Object val, boolean custom) throws 
IOException {
+        if (val != null) {
+            buffer.append(' ');
+            if (custom) {
+                buffer.append('_');
+            }
+            buffer.append(key);
+            buffer.append('=');
+            if (val instanceof Number) {
+                if (val instanceof Long) {
+                    buffer.format(((Long) val).longValue());
+                } else if (val instanceof Integer) {
+                    buffer.format(((Integer) val).intValue());
+                } else {
+                    buffer.append(val.toString());
+                }
+            } else {
+                buffer.append('"');
+                buffer.format(val);
+                buffer.append('"');
+            }
+        }
+    }
+
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/layout/Rfc3164Layout.java 
b/audit/src/main/java/org/apache/karaf/audit/layout/Rfc3164Layout.java
new file mode 100644
index 0000000..0f77985
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/layout/Rfc3164Layout.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.layout;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.util.Buffer;
+import org.apache.karaf.audit.util.FastDateFormat;
+
+import java.io.IOException;
+
+public class Rfc3164Layout extends AbstractLayout {
+
+    public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
+
+    protected final int facility;
+    protected final int priority;
+    protected final int enterpriseNumber;
+
+    protected String hdr1;
+    protected String hdr2;
+    protected String hdr3;
+
+    protected FastDateFormat fastDateFormat = new FastDateFormat();
+
+    public Rfc3164Layout(int facility, int priority, int enterpriseNumber) {
+        super(new Buffer(Buffer.Format.Syslog));
+        this.facility = facility;
+        this.priority = priority;
+        this.enterpriseNumber = enterpriseNumber;
+
+        hdr1 = "<" + ((facility << 3) + priority) + ">";
+        hdr2 = " " + hostName + " " + appName + " " + procId + " ";
+        hdr3 = enterpriseNumber > 0 ? "@" + enterpriseNumber : "";
+    }
+
+    @Override
+    protected void header(Event event) throws IOException {
+        buffer.append(hdr1);
+        datetime(event.timestamp());
+        buffer.append(hdr2);
+        buffer.append(event.type());
+        buffer.append(' ');
+        buffer.append('[');
+        buffer.append(event.type());
+        buffer.append(hdr3);
+    }
+
+    @Override
+    protected void footer(Event event) throws IOException {
+        buffer.append(']');
+    }
+
+    @Override
+    protected void append(String key, Object val) throws IOException {
+        if (val != null) {
+            buffer.append(' ')
+                    .append(key)
+                    .append('=')
+                    .append('"')
+                    .format(val)
+                    .append('"');
+        }
+    }
+
+    protected void datetime(long millis) throws IOException {
+        buffer.append(fastDateFormat.getDate(millis, FastDateFormat.MMM_D2));
+        buffer.append(' ');
+        fastDateFormat.writeTime(millis, false, buffer);
+    }
+
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/layout/Rfc5424Layout.java 
b/audit/src/main/java/org/apache/karaf/audit/layout/Rfc5424Layout.java
new file mode 100644
index 0000000..5bd314b
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/layout/Rfc5424Layout.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.layout;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.util.Buffer;
+import org.apache.karaf.audit.util.FastDateFormat;
+
+import java.io.IOException;
+
+public class Rfc5424Layout extends AbstractLayout {
+
+    public static final int DEFAULT_ENTERPRISE_NUMBER = 18060;
+
+    protected final int facility;
+    protected final int priority;
+    protected final int enterpriseNumber;
+
+    protected String hdr1;
+    protected String hdr2;
+    protected String hdr3;
+
+    protected FastDateFormat fastDateFormat = new FastDateFormat();
+
+    public Rfc5424Layout(int facility, int priority, int enterpriseNumber) {
+        super(new Buffer(Buffer.Format.Syslog));
+        this.facility = facility;
+        this.priority = priority;
+        this.enterpriseNumber = enterpriseNumber;
+
+        hdr1 = "<" + ((facility << 3) + priority) + ">1 ";
+        hdr2 = " " + hostName + " " + appName + " " + procId + " ";
+        hdr3 = enterpriseNumber > 0 ? "@" + enterpriseNumber : "";
+    }
+
+    @Override
+    protected void header(Event event) throws IOException {
+        buffer.append(hdr1);
+        datetime(event.timestamp());
+        buffer.append(hdr2);
+        buffer.append(event.type());
+        buffer.append(' ');
+        buffer.append('[');
+        buffer.append(event.type());
+        buffer.append(hdr3);
+    }
+
+    @Override
+    protected void footer(Event event) throws IOException {
+        buffer.append(']');
+    }
+
+    @Override
+    protected void append(String key, Object val) throws IOException {
+        if (val != null) {
+            buffer.append(' ')
+                    .append(key)
+                    .append('=')
+                    .append('"')
+                    .format(val)
+                    .append('"');
+        }
+    }
+
+    protected void datetime(long millis) throws IOException {
+        buffer.append(fastDateFormat.getDate(millis, 
FastDateFormat.YYYY_MM_DD));
+        buffer.append('T');
+        fastDateFormat.writeTime(millis, true, buffer);
+        buffer.append(fastDateFormat.getDate(millis, FastDateFormat.XXX));
+    }
+
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/layout/SimpleLayout.java 
b/audit/src/main/java/org/apache/karaf/audit/layout/SimpleLayout.java
new file mode 100644
index 0000000..deb95d4
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/layout/SimpleLayout.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.layout;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.util.Buffer;
+import org.apache.karaf.audit.util.FastDateFormat;
+
+import java.io.IOException;
+
+public class SimpleLayout extends AbstractLayout {
+
+    protected String hdr;
+
+    protected FastDateFormat fastDateFormat = new FastDateFormat();
+
+    public SimpleLayout() {
+        super(new Buffer(Buffer.Format.Json));
+        hdr = " " + hostName + " " + appName + " " + procId + " ";
+    }
+
+    @Override
+    protected void header(Event event) throws IOException {
+        datetime(event.timestamp());
+        buffer.append(hdr);
+    }
+
+    @Override
+    protected void footer(Event event) throws IOException {
+    }
+
+    @Override
+    protected void append(String key, Object val) throws IOException {
+        if (val != null) {
+            switch (key) {
+                case "subject":
+                case "type":
+                case "subtype":
+                    buffer.append(' ').format(val);
+                    break;
+                default:
+                    buffer.append(' 
').append(key).append('=').append('"').format(val).append('"');
+                    break;
+            }
+        }
+    }
+
+    protected void datetime(long millis) throws IOException {
+        buffer.append(fastDateFormat.getDate(millis, 
FastDateFormat.YYYY_MM_DD));
+        buffer.append('T');
+        fastDateFormat.writeTime(millis, true, buffer);
+        buffer.append(fastDateFormat.getDate(millis, FastDateFormat.XXX));
+    }
+
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/logger/FileEventLogger.java 
b/audit/src/main/java/org/apache/karaf/audit/logger/FileEventLogger.java
new file mode 100644
index 0000000..dfe3ee3
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/logger/FileEventLogger.java
@@ -0,0 +1,291 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.logger;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.EventLayout;
+import org.apache.karaf.audit.EventLogger;
+import org.apache.karaf.audit.util.FastDateFormat;
+
+import java.io.BufferedWriter;
+import java.io.Closeable;
+import java.io.Flushable;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.stream.Collectors;
+import java.util.zip.GZIPOutputStream;
+
+public class FileEventLogger implements EventLogger {
+
+    private final Charset encoding;
+    private final String policy;
+    private final int files;
+    private final boolean compress;
+    private final Executor executor;
+    private final EventLayout layout;
+    private boolean daily;
+    private long maxSize;
+    private long size;
+    private Path path;
+    private Writer writer;
+    private FastDateFormat fastDateFormat = new FastDateFormat();
+
+    public FileEventLogger(String path, String encoding, String policy, int 
files, boolean compress, ThreadFactory factory, EventLayout layout) throws 
IOException {
+        this.path = Paths.get(path);
+        this.encoding = Charset.forName(encoding);
+        this.policy = policy;
+        this.files = files;
+        this.compress = compress;
+        this.executor = Executors.newSingleThreadExecutor(factory);
+        this.layout = layout;
+        Files.createDirectories(this.path.getParent());
+
+        for (String pol : policy.toLowerCase(Locale.ENGLISH).split("\\s+")) {
+            if ("daily".equals(pol)) {
+                daily = true;
+            } else if (pol.matches("size\\([0-9]+(kb|mb|gb)?\\)")) {
+                String str = pol.substring(5, pol.length() - 1);
+                long mult;
+                if (str.endsWith("kb")) {
+                    mult = 1024;
+                    str = str.substring(0, str.length() - 2);
+                } else if (str.endsWith("mb")) {
+                    mult = 1024 * 1024;
+                    str = str.substring(0, str.length() - 2);
+                } else if (str.endsWith("gb")) {
+                    mult = 1024 * 1024 * 1024;
+                    str = str.substring(0, str.length() - 2);
+                } else {
+                    mult = 1;
+                }
+                try {
+                    maxSize = Long.parseLong(str) * mult;
+                } catch (NumberFormatException t) {
+                    // ignore
+                }
+                if (maxSize <= 0) {
+                    throw new IllegalArgumentException("Unsupported policy: " 
+ pol);
+                }
+            } else {
+                throw new IllegalArgumentException("Unsupported policy: " + 
pol);
+            }
+        }
+    }
+
+    @Override
+    public void write(Event event) throws IOException {
+        long timestamp = event.timestamp();
+        if (writer == null) {
+            init();
+        } else {
+            check(timestamp);
+        }
+        layout.format(event, writer);
+        writer.append("\n");
+    }
+
+    private void init() throws IOException {
+        long timestamp = System.currentTimeMillis();
+        if (Files.isRegularFile(path)) {
+            size = Files.size(path);
+            fastDateFormat.sameDay(Files.getLastModifiedTime(path).toMillis());
+            if (trigger(timestamp)) {
+                Path temp = Files.createTempFile(path.getParent(), 
path.getFileName().toString(), ".tmp");
+                Files.move(path, temp);
+                executor.execute(() -> rotate(temp, timestamp));
+            }
+        }
+        fastDateFormat.sameDay(timestamp);
+        writer = new Writer(Files.newBufferedWriter(path, encoding, 
StandardOpenOption.CREATE, StandardOpenOption.APPEND));
+        size = 0;
+    }
+
+
+    private void check(long timestamp) throws IOException {
+        if (trigger(timestamp)) {
+            if (writer != null) {
+                writer.flush();
+                if (Files.size(path) == 0) {
+                    return;
+                }
+                writer.close();
+            }
+            Path temp = Files.createTempFile(path.getParent(), 
path.getFileName().toString() + ".", ".tmp");
+            Files.delete(temp);
+            Files.move(path, temp, StandardCopyOption.ATOMIC_MOVE);
+            executor.execute(() -> rotate(temp, timestamp));
+            writer = new Writer(Files.newBufferedWriter(path, encoding, 
StandardOpenOption.CREATE, StandardOpenOption.APPEND));
+            size = 0;
+        }
+    }
+
+    private boolean trigger(long timestamp) {
+        return maxSize > 0 && size > maxSize
+                || daily && !fastDateFormat.sameDay(timestamp);
+    }
+
+    private void rotate(Path path, long timestamp) {
+        try {
+            // Compute final name
+            String[] fix = getFileNameFix();
+            List<String> paths = Files.list(path.getParent())
+                    .filter(p -> !p.equals(this.path))
+                    .map(Path::getFileName)
+                    .map(Path::toString)
+                    .filter(p -> p.startsWith(fix[0]))
+                    .filter(p -> !p.endsWith(".tmp"))
+                    .collect(Collectors.toList());
+            String date = new FastDateFormat().getDate(timestamp, 
FastDateFormat.YYYY_MM_DD);
+            List<String> sameDate = paths.stream()
+                    .filter(p -> p.matches("\\Q" + fix[0] + "-" + date + 
"\\E(-[0-9]+)?\\Q" + fix[1] + "\\E"))
+                    .collect(Collectors.toList());
+            String name = fix[0] + "-" + date + fix[1];
+            int idx = 0;
+            while (sameDate.contains(name)) {
+                name = fix[0] + "-" + date + "-" + Integer.toString(++idx) + 
fix[1];
+            }
+            paths.add(name);
+            Path finalPath = path.resolveSibling(name);
+            // Compress or move the file
+            if (compress) {
+                try (OutputStream out = Files.newOutputStream(finalPath, 
StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
+                     GZIPOutputStream zip = new GZIPOutputStream(out)) {
+                    Files.copy(path, zip);
+                }
+                Files.delete(path);
+            } else {
+                Files.move(path, finalPath);
+            }
+            // Check number of files
+            if (files > 0 && paths.size() > files) {
+                Collections.sort(paths);
+                paths.subList(paths.size() - files, paths.size()).clear();
+                for (String p : paths) {
+                    Files.delete(path.resolveSibling(p));
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+    }
+
+    private String[] getFileNameFix() {
+        String str = path.getFileName().toString();
+        String sfx = compress ? ".gz": "";
+        int idx = str.lastIndexOf('.');
+        if (idx > 0) {
+            return new String[] { str.substring(0, idx), str.substring(idx) + 
sfx };
+        } else {
+            return new String[] { str, sfx };
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        if (writer != null) {
+            writer.flush();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (writer != null) {
+            writer.close();
+        }
+    }
+
+    class Writer extends java.io.Writer implements Appendable, Closeable, 
Flushable {
+        private final BufferedWriter writer;
+
+        public Writer(BufferedWriter writer) {
+            this.writer = writer;
+        }
+
+        @Override
+        public void flush() throws IOException {
+            writer.flush();
+        }
+
+        @Override
+        public void close() throws IOException {
+            writer.close();
+        }
+
+        @Override
+        public void write(int c) throws IOException {
+            size += 1;
+            writer.write(c);
+        }
+
+        @Override
+        public void write(char[] cbuf, int off, int len) throws IOException {
+            size += len;
+            writer.write(cbuf, off, len);
+        }
+
+        @Override
+        public void write(String s, int off, int len) throws IOException {
+            size += len;
+            writer.write(s, off, len);
+        }
+
+        @Override
+        public void write(char[] cbuf) throws IOException {
+            size += cbuf.length;
+            writer.write(cbuf);
+        }
+
+        @Override
+        public void write(String str) throws IOException {
+            size += str.length();
+            writer.write(str);
+        }
+
+        @Override
+        public java.io.Writer append(CharSequence csq) throws IOException {
+            size += csq.length();
+            writer.append(csq);
+            return this;
+        }
+
+        @Override
+        public java.io.Writer append(CharSequence csq, int start, int end) 
throws IOException {
+            size += end - start;
+            writer.append(csq, start, end);
+            return this;
+        }
+
+        @Override
+        public java.io.Writer append(char c) throws IOException {
+            size += 1;
+            writer.append(c);
+            return this;
+        }
+    }
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/logger/JulEventLogger.java 
b/audit/src/main/java/org/apache/karaf/audit/logger/JulEventLogger.java
new file mode 100644
index 0000000..9b6fe55
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/logger/JulEventLogger.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.logger;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.EventLayout;
+import org.apache.karaf.audit.EventLogger;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class JulEventLogger implements EventLogger {
+
+    private final String logger;
+    private final Level level;
+    private final EventLayout layout;
+
+    public JulEventLogger(String logger, String level, EventLayout layout) {
+        this.logger = logger;
+        this.level = Level.parse(level.toUpperCase(Locale.ENGLISH));
+        this.layout = layout;
+    }
+
+    @Override
+    public void write(Event event) throws IOException {
+        getLogger(event.type() + "." + event.subtype())
+                .log(getLevel(event), layout.format(event).toString());
+    }
+
+    protected Level getLevel(Event event) {
+        return level;
+    }
+
+    protected Logger getLogger(String t) {
+        return Logger.getLogger(this.logger + "." + t);
+    }
+
+    @Override
+    public void flush() throws IOException {
+    }
+
+    @Override
+    public void close() throws IOException {
+    }
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/logger/TcpEventLogger.java 
b/audit/src/main/java/org/apache/karaf/audit/logger/TcpEventLogger.java
new file mode 100644
index 0000000..b5fbef8
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/logger/TcpEventLogger.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.logger;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.EventLayout;
+import org.apache.karaf.audit.EventLogger;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.nio.charset.Charset;
+
+public class TcpEventLogger implements EventLogger {
+
+    private final String host;
+    private final int port;
+    private final Charset encoding;
+    private final EventLayout layout;
+    private BufferedWriter writer;
+
+    public TcpEventLogger(String host, int port, String encoding, EventLayout 
layout) throws IOException {
+        this.host = host;
+        this.port = port;
+        this.encoding = Charset.forName(encoding);
+        this.layout = layout;
+    }
+
+    @Override
+    public void write(Event event) throws IOException {
+        if (writer == null) {
+            Socket socket = new Socket(host, port);
+            this.writer = new BufferedWriter(new 
OutputStreamWriter(socket.getOutputStream(), encoding));
+        }
+        layout.format(event, writer);
+        writer.append("\n");
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (writer != null) {
+            writer.close();
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        if (writer != null) {
+            writer.flush();
+        }
+    }
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/logger/UdpEventLogger.java 
b/audit/src/main/java/org/apache/karaf/audit/logger/UdpEventLogger.java
new file mode 100644
index 0000000..c4e8a3a
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/logger/UdpEventLogger.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.logger;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.EventLayout;
+import org.apache.karaf.audit.EventLogger;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+
+public class UdpEventLogger implements EventLogger {
+
+    private final InetAddress host;
+    private final int port;
+    private final CharsetEncoder encoder;
+    private final EventLayout layout;
+    private final DatagramSocket dgram;
+
+    private ByteBuffer bb = ByteBuffer.allocate(1024);
+
+    public UdpEventLogger(String host, int port, String encoding, EventLayout 
layout) throws SocketException, UnknownHostException {
+        this.layout = layout;
+        this.host = InetAddress.getByName(host);
+        this.port = port;
+        this.encoder = Charset.forName(encoding).newEncoder();
+        this.dgram = new DatagramSocket();
+    }
+
+    @Override
+    public void write(Event event) throws IOException {
+        CharBuffer cb = layout.format(event);
+        int cap = (int) (cb.remaining() * encoder.averageBytesPerChar());
+        ByteBuffer bb;
+        if (this.bb.capacity() > cap) {
+            bb = this.bb;
+        } else {
+            bb = ByteBuffer.allocate(cap);
+        }
+        encoder.reset();
+        encoder.encode(cb, bb, true);
+        if (cb.remaining() > 0) {
+            bb = ByteBuffer.allocate(bb.capacity() * 2);
+            cb.position(0);
+            encoder.reset();
+            encoder.encode(cb, bb, true);
+        }
+
+        dgram.send(new DatagramPacket(bb.array(), 0, bb.position(), host, 
port));
+        bb.position(0);
+    }
+
+    @Override
+    public void flush() throws IOException {
+    }
+
+    @Override
+    public void close() throws IOException {
+        dgram.close();
+    }
+
+}
diff --git a/audit/src/main/java/org/apache/karaf/audit/util/Buffer.java 
b/audit/src/main/java/org/apache/karaf/audit/util/Buffer.java
new file mode 100644
index 0000000..8779357
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/util/Buffer.java
@@ -0,0 +1,306 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.util;
+
+import org.apache.karaf.jaas.boot.principal.ClientPrincipal;
+import org.apache.karaf.jaas.boot.principal.UserPrincipal;
+
+import javax.security.auth.Subject;
+import java.io.IOException;
+import java.io.Writer;
+import java.security.Principal;
+
+public final class Buffer implements Appendable, CharSequence {
+
+    public enum Format {
+        Json, Syslog
+    }
+
+    private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', 
'6', '7',
+                                       '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' 
};
+
+    protected final Format format;
+    protected final int capacity;
+    protected char[] buffer;
+    protected int position = 0;
+
+    public Buffer(Format format) {
+        this(format, 1024);
+    }
+
+    public Buffer(Format format, int size) {
+        this.format = format;
+        this.capacity = size;
+        this.buffer = new char[size];
+    }
+
+    public char[] buffer() {
+        return buffer;
+    }
+
+    public int position() {
+        return position;
+    }
+
+    public void clear() {
+        position = 0;
+        if (this.buffer.length > capacity) {
+            this.buffer = new char[capacity];
+        }
+    }
+
+    public String toString() {
+        return new String(buffer, 0, position);
+    }
+
+    public void writeTo(Appendable out) throws IOException {
+        if (out instanceof Writer) {
+            ((Writer) out).write(buffer, 0, position);
+        } else if (out instanceof StringBuilder) {
+            ((StringBuilder) out).append(buffer, 0, position);
+        } else {
+            out.append(this);
+        }
+    }
+
+    private final void require(int nb) {
+        if (position + nb >= buffer.length) {
+            char[] b = new char[buffer.length * 2];
+            System.arraycopy(buffer, 0, b, 0, position);
+            buffer = b;
+        }
+    }
+
+    @Override
+    public Buffer append(CharSequence csq) throws IOException {
+        return append(csq, 0, csq.length());
+    }
+
+    @Override
+    public Buffer append(CharSequence csq, int start, int end) throws 
IOException {
+        if (csq instanceof String) {
+            return append((String) csq, start, end);
+        } else {
+            require(end - start);
+            for (int i = start; i < end; i++) {
+                buffer[position++] = csq.charAt(i);
+            }
+            return this;
+        }
+    }
+
+    public Buffer append(String str) throws IOException {
+        return append(str, 0, str.length());
+    }
+
+    public Buffer append(String str, int start, int end) throws IOException {
+        int nb = end - start;
+        require(nb);
+        str.getChars(start, end, buffer, position);
+        position += nb;
+        return this;
+    }
+
+    @Override
+    public Buffer append(char c) throws IOException {
+        require(1);
+        buffer[position++] = c;
+        return this;
+    }
+
+    @Override
+    public int length() {
+        return position;
+    }
+
+    @Override
+    public char charAt(int index) {
+        return buffer[index];
+    }
+
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        return new String(buffer, start, end);
+    }
+
+    public Buffer format(Object object) throws IOException {
+        if (object == null) {
+            require(4);
+            buffer[position++] = 'n';
+            buffer[position++] = 'u';
+            buffer[position++] = 'l';
+            buffer[position++] = 'l';
+            return this;
+        } else if (object.getClass().isArray()) {
+            return format((Object[]) object);
+        } else if (object instanceof Subject) {
+            return format((Subject) object);
+        } else {
+            return format(object.toString());
+        }
+    }
+
+    public Buffer format(Object[] array) throws IOException {
+        require(array.length * 10);
+        buffer[position++] = '[';
+        for (int i = 0; i < array.length; i++) {
+            if (i > 0) {
+                buffer[position++] = ',';
+                buffer[position++] = ' ';
+            }
+            format(array[i]);
+        }
+        if (format == Format.Syslog) {
+            buffer[position++] = '\\';
+        }
+        buffer[position++] = ']';
+        return this;
+    }
+
+    public Buffer format(Subject subject) throws IOException {
+        String up = null;
+        String cp = null;
+        for (Principal p : subject.getPrincipals()) {
+            if (p instanceof UserPrincipal) {
+                up = p.getName();
+            } else if (p instanceof ClientPrincipal) {
+                cp = p.getName();
+            }
+        }
+        if (up != null) {
+            append(up);
+        } else {
+            append('?');
+        }
+        if (cp != null) {
+            append('@');
+            append(cp);
+        }
+        return this;
+    }
+
+    public Buffer format(String cs) throws IOException {
+        switch (format) {
+            case Json:
+                formatJson(cs);
+                break;
+            case Syslog:
+                formatSyslog(cs);
+                break;
+        }
+        return this;
+    }
+
+    public Buffer format(int i) throws IOException {
+        require(11);
+        position = NumberOutput.outputInt(i, buffer, position);
+        return this;
+    }
+
+    public Buffer format(long i) throws IOException {
+        require(20);
+        position = NumberOutput.outputLong(i, buffer, position);
+        return this;
+    }
+
+    private void formatJson(String value) throws IOException {
+        int len = value.length();
+        require(len * 4);
+        position = transferJson(position, buffer, value, 0, len);
+    }
+
+    private void formatSyslog(String value) throws IOException {
+        int end = value.length();
+        int max = Math.min(end, 255);
+        require(max * 4);
+        position = transferSyslog(position, buffer, value, 0, max);
+        if (end > max) {
+            require(3);
+            buffer[position++] = '.';
+            buffer[position++] = '.';
+            buffer[position++] = '.';
+        }
+    }
+
+    private int transferJson(int position, char[] d, String s, int start, int 
end) {
+        for (int i = start; i < end; i++) {
+            char c = s.charAt(i);
+            switch (c) {
+                case '\"':
+                    d[position++] = '\\';
+                    d[position++] = '"';
+                    break;
+                case '\\':
+                    d[position++] = '\\';
+                    d[position++] = '\\';
+                    break;
+                case '\b':
+                    d[position++] = '\\';
+                    d[position++] = 'b';
+                    break;
+                case '\f':
+                    d[position++] = '\\';
+                    d[position++] = 'f';
+                    break;
+                case '\n':
+                    d[position++] = '\\';
+                    d[position++] = 'n';
+                    break;
+                case '\r':
+                    d[position++] = '\\';
+                    d[position++] = 'r';
+                    break;
+                case '\t':
+                    d[position++] = '\\';
+                    d[position++] = 't';
+                    break;
+                default:
+                    if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || (c >= 
'\u2000' && c < '\u2100')) {
+                        d[position++] = '\\';
+                        d[position++] = 'u';
+                        d[position++] = HEX_DIGITS[c >> 12];
+                        d[position++] = HEX_DIGITS[(c >> 8) & 0x0F];
+                        d[position++] = HEX_DIGITS[(c >> 4) & 0x0F];
+                        d[position++] = HEX_DIGITS[c & 0x0F];
+                    } else {
+                        d[position++] = c;
+                    }
+                    break;
+            }
+        }
+        return position;
+    }
+
+    private int transferSyslog(int position, char[] d, String s, int start, 
int end) {
+        for (int i = start; i < end; i++) {
+            char c = s.charAt(i);
+            switch (c) {
+                case '"':
+                case '\\':
+                case ']':
+                    d[position++] = '\\';
+                    d[position++] = c;
+                    break;
+                default:
+                    d[position++] = c;
+                    break;
+            }
+        }
+        return position;
+    }
+
+}
diff --git 
a/audit/src/main/java/org/apache/karaf/audit/util/FastDateFormat.java 
b/audit/src/main/java/org/apache/karaf/audit/util/FastDateFormat.java
new file mode 100644
index 0000000..968e4a7
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/util/FastDateFormat.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.util;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.FieldPosition;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+public class FastDateFormat {
+
+    public static final String YYYY_MM_DD = "yyyy-MM-dd";
+    public static final String MMM_D2 = "MMM d2";
+    public static final String XXX = "XXX";
+
+    private final TimeZone timeZone;
+    private final Locale locale;
+    private long midnightTomorrow;
+    private long midnightToday;
+    private final int[] dstOffsets = new int[25];
+
+    private Map<String, String> cache = new HashMap<>();
+
+    public FastDateFormat() {
+        this(TimeZone.getDefault(), Locale.ENGLISH);
+    }
+
+    public FastDateFormat(TimeZone timeZone, Locale locale) {
+        this.timeZone = timeZone;
+        this.locale = locale;
+    }
+
+    /**
+     * Check whether the given instant if in the same day as the previous one.
+     */
+    public boolean sameDay(long now) {
+        if (now >= midnightTomorrow || now < midnightToday) {
+            updateMidnightMillis(now);
+            updateDaylightSavingTime();
+            cache.clear();
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Get the date formatted with the given pattern.
+     */
+    public String getDate(long now, String pattern) {
+        sameDay(now);
+        String date = cache.get(pattern);
+        if (date == null) {
+            if (MMM_D2.equals(pattern)) {
+                StringBuffer sb = new StringBuffer();
+                FieldPosition fp = new 
FieldPosition(DateFormat.Field.DAY_OF_MONTH);
+                new SimpleDateFormat("MMM dd", locale).format(new Date(now), 
sb, fp);
+                if (sb.charAt(fp.getBeginIndex()) == '0') {
+                    sb.setCharAt(fp.getBeginIndex(), ' ');
+                }
+                date = sb.toString();
+            } else {
+                date = new SimpleDateFormat(pattern, locale).format(new 
Date(now));
+            }
+            cache.put(pattern, date);
+        }
+        return date;
+    }
+
+    /**
+     * Write the time in the HH:MM:SS[.sss] format to the given 
<code>Appendable</code>.
+     */
+    public void writeTime(long now, boolean writeMillis, Appendable buffer) 
throws IOException {
+        int ms = millisSinceMidnight(now);
+
+        final int hourOfDay = ms / 3600000;
+        final int hours = hourOfDay + daylightSavingTime(hourOfDay) / 3600000;
+        ms -= 3600000 * hourOfDay;
+
+        final int minutes = ms / 60000;
+        ms -= 60000 * minutes;
+
+        final int seconds = ms / 1000;
+        ms -= 1000 * seconds;
+
+        // Hour
+        int temp = hours / 10;
+        buffer.append((char) (temp + '0'));
+        buffer.append ((char) (hours - 10 * temp + '0'));
+        buffer.append(':');
+
+        // Minute
+        temp = minutes / 10;
+        buffer.append((char) (temp + '0'));
+        buffer.append((char) (minutes - 10 * temp + '0'));
+        buffer.append(':');
+
+        // Second
+        temp = seconds / 10;
+        buffer.append((char) (temp + '0'));
+        buffer.append((char) (seconds - 10 * temp + '0'));
+
+        // Millisecond
+        if (writeMillis) {
+            buffer.append('.');
+            temp = ms / 100;
+            buffer.append((char) (temp + '0'));
+            ms -= 100 * temp;
+            temp = ms / 10;
+            buffer.append((char) (temp + '0'));
+            ms -= 10 * temp;
+            buffer.append((char) (ms + '0'));
+        }
+    }
+
+    private int millisSinceMidnight(final long now) {
+        sameDay(now);
+        return (int) (now - midnightToday);
+    }
+
+    private int daylightSavingTime(final int hourOfDay) {
+        return hourOfDay > 23 ? dstOffsets[23] : dstOffsets[hourOfDay];
+    }
+
+    private void updateMidnightMillis(final long now) {
+        final Calendar cal = Calendar.getInstance(timeZone);
+        cal.setTimeInMillis(now);
+        cal.set(Calendar.HOUR_OF_DAY, 0);
+        cal.set(Calendar.MINUTE, 0);
+        cal.set(Calendar.SECOND, 0);
+        cal.set(Calendar.MILLISECOND, 0);
+        midnightToday = cal.getTimeInMillis();
+        cal.add(Calendar.DATE, 1);
+        midnightTomorrow = cal.getTimeInMillis();
+    }
+
+    private void updateDaylightSavingTime() {
+        Arrays.fill(dstOffsets, 0);
+        final int ONE_HOUR = (int) TimeUnit.HOURS.toMillis(1);
+        if (timeZone.getOffset(midnightToday) != 
timeZone.getOffset(midnightToday + 23 * ONE_HOUR)) {
+            for (int i = 0; i < dstOffsets.length; i++) {
+                final long time = midnightToday + i * ONE_HOUR;
+                dstOffsets[i] = timeZone.getOffset(time) - 
timeZone.getRawOffset();
+            }
+            if (dstOffsets[0] > dstOffsets[23]) { // clock is moved backwards.
+                // we obtain midnightTonight with 
Calendar.getInstance(TimeZone), so it already includes raw offset
+                for (int i = dstOffsets.length - 1; i >= 0; i--) {
+                    dstOffsets[i] -= dstOffsets[0]; //
+                }
+            }
+        }
+    }
+}
diff --git a/audit/src/main/java/org/apache/karaf/audit/util/NumberOutput.java 
b/audit/src/main/java/org/apache/karaf/audit/util/NumberOutput.java
new file mode 100644
index 0000000..28e5fb7
--- /dev/null
+++ b/audit/src/main/java/org/apache/karaf/audit/util/NumberOutput.java
@@ -0,0 +1,516 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.util;
+
+/**
+ * Code copied from org.codehaus.jackson.io.NumberOutput
+ */
+public final class NumberOutput
+{
+    private static int MILLION = 1000000;
+    private static int BILLION = 1000000000;
+    private static long BILLION_L = 1000000000L;
+
+    private static long MIN_INT_AS_LONG = (long) Integer.MIN_VALUE;
+    private static long MAX_INT_AS_LONG = (long) Integer.MAX_VALUE;
+
+    final static String SMALLEST_INT = String.valueOf(Integer.MIN_VALUE);
+    final static String SMALLEST_LONG = String.valueOf(Long.MIN_VALUE);
+
+    /**
+     * Encoded representations of 3-decimal-digit indexed values, where
+     * 3 LSB are ascii characters
+     *
+     * @since 2.8.2
+     */
+    private final static int[] TRIPLET_TO_CHARS = new int[1000];
+
+    static {
+        /* Let's fill it with NULLs for ignorable leading digits,
+         * and digit chars for others
+         */
+        int fullIx = 0;
+        for (int i1 = 0; i1 < 10; ++i1) {
+            for (int i2 = 0; i2 < 10; ++i2) {
+                for (int i3 = 0; i3 < 10; ++i3) {
+                    int enc = ((i1 + '0') << 16)
+                            | ((i2 + '0') << 8)
+                            | (i3 + '0');
+                    TRIPLET_TO_CHARS[fullIx++] = enc;
+                }
+            }
+        }
+    }
+
+    private final static String[] sSmallIntStrs = new String[] {
+            "0","1","2","3","4","5","6","7","8","9","10"
+    };
+    private final static String[] sSmallIntStrs2 = new String[] {
+            "-1","-2","-3","-4","-5","-6","-7","-8","-9","-10"
+    };
+
+    /*
+    /**********************************************************
+    /* Efficient serialization methods using raw buffers
+    /**********************************************************
+     */
+
+    /**
+     * @return Offset within buffer after outputting int
+     */
+    public static int outputInt(int v, char[] b, int off)
+    {
+        if (v < 0) {
+            if (v == Integer.MIN_VALUE) {
+                // Special case: no matching positive value within range;
+                // let's then "upgrade" to long and output as such.
+                return _outputSmallestI(b, off);
+            }
+            b[off++] = '-';
+            v = -v;
+        }
+
+        if (v < MILLION) { // at most 2 triplets...
+            if (v < 1000) {
+                if (v < 10) {
+                    b[off] = (char) ('0' + v);
+                    return off+1;
+                }
+                return _leading3(v, b, off);
+            }
+            int thousands = v / 1000;
+            v -= (thousands * 1000); // == value % 1000
+            off = _leading3(thousands, b, off);
+            off = _full3(v, b, off);
+            return off;
+        }
+
+        // ok, all 3 triplets included
+        /* Let's first hand possible billions separately before
+         * handling 3 triplets. This is possible since we know we
+         * can have at most '2' as billion count.
+         */
+        if (v >= BILLION) {
+            v -= BILLION;
+            if (v >= BILLION) {
+                v -= BILLION;
+                b[off++] = '2';
+            } else {
+                b[off++] = '1';
+            }
+            return _outputFullBillion(v, b, off);
+        }
+        int newValue = v / 1000;
+        int ones = (v - (newValue * 1000)); // == value % 1000
+        v = newValue;
+        newValue /= 1000;
+        int thousands = (v - (newValue * 1000));
+
+        off = _leading3(newValue, b, off);
+        off = _full3(thousands, b, off);
+        return _full3(ones, b, off);
+    }
+
+    public static int outputInt(int v, byte[] b, int off)
+    {
+        if (v < 0) {
+            if (v == Integer.MIN_VALUE) {
+                return _outputSmallestI(b, off);
+            }
+            b[off++] = '-';
+            v = -v;
+        }
+
+        if (v < MILLION) { // at most 2 triplets...
+            if (v < 1000) {
+                if (v < 10) {
+                    b[off++] = (byte) ('0' + v);
+                } else {
+                    off = _leading3(v, b, off);
+                }
+            } else {
+                int thousands = v / 1000;
+                v -= (thousands * 1000); // == value % 1000
+                off = _leading3(thousands, b, off);
+                off = _full3(v, b, off);
+            }
+            return off;
+        }
+        if (v >= BILLION) {
+            v -= BILLION;
+            if (v >= BILLION) {
+                v -= BILLION;
+                b[off++] = '2';
+            } else {
+                b[off++] = '1';
+            }
+            return _outputFullBillion(v, b, off);
+        }
+        int newValue = v / 1000;
+        int ones = (v - (newValue * 1000)); // == value % 1000
+        v = newValue;
+        newValue /= 1000;
+        int thousands = (v - (newValue * 1000));
+        off = _leading3(newValue, b, off);
+        off = _full3(thousands, b, off);
+        return _full3(ones, b, off);
+    }
+
+    /**
+     * @return Offset within buffer after outputting int
+     */
+    public static int outputLong(long v, char[] b, int off)
+    {
+        // First: does it actually fit in an int?
+        if (v < 0L) {
+            if (v > MIN_INT_AS_LONG) {
+                return outputInt((int) v, b, off);
+            }
+            if (v == Long.MIN_VALUE) {
+                return _outputSmallestL(b, off);
+            }
+            b[off++] = '-';
+            v = -v;
+        } else {
+            if (v <= MAX_INT_AS_LONG) {
+                return outputInt((int) v, b, off);
+            }
+        }
+
+        // Ok, let's separate last 9 digits (3 x full sets of 3)
+        long upper = v / BILLION_L;
+        v -= (upper * BILLION_L);
+
+        // two integers?
+        if (upper < BILLION_L) {
+            off = _outputUptoBillion((int) upper, b, off);
+        } else {
+            // no, two ints and bits; hi may be about 16 or so
+            long hi = upper / BILLION_L;
+            upper -= (hi * BILLION_L);
+            off = _leading3((int) hi, b, off);
+            off = _outputFullBillion((int) upper, b, off);
+        }
+        return _outputFullBillion((int) v, b, off);
+    }
+
+    public static int outputLong(long v, byte[] b, int off)
+    {
+        if (v < 0L) {
+            if (v > MIN_INT_AS_LONG) {
+                return outputInt((int) v, b, off);
+            }
+            if (v == Long.MIN_VALUE) {
+                return _outputSmallestL(b, off);
+            }
+            b[off++] = '-';
+            v = -v;
+        } else {
+            if (v <= MAX_INT_AS_LONG) {
+                return outputInt((int) v, b, off);
+            }
+        }
+
+        // Ok, let's separate last 9 digits (3 x full sets of 3)
+        long upper = v / BILLION_L;
+        v -= (upper * BILLION_L);
+
+        // two integers?
+        if (upper < BILLION_L) {
+            off = _outputUptoBillion((int) upper, b, off);
+        } else {
+            // no, two ints and bits; hi may be about 16 or so
+            long hi = upper / BILLION_L;
+            upper -= (hi * BILLION_L);
+            off = _leading3((int) hi, b, off);
+            off = _outputFullBillion((int) upper, b, off);
+        }
+        return _outputFullBillion((int) v, b, off);
+    }
+
+    /*
+    /**********************************************************
+    /* Convenience serialization methods
+    /**********************************************************
+     */
+
+    /* !!! 05-Aug-2008, tatus: Any ways to further optimize
+     *   these? (or need: only called by diagnostics methods?)
+     */
+    public static String toString(int v)
+    {
+        // Lookup table for small values
+        if (v < sSmallIntStrs.length) {
+            if (v >= 0) {
+                return sSmallIntStrs[v];
+            }
+            int v2 = -v - 1;
+            if (v2 < sSmallIntStrs2.length) {
+                return sSmallIntStrs2[v2];
+            }
+        }
+        return Integer.toString(v);
+    }
+
+    public static String toString(long v) {
+        if (v <= Integer.MAX_VALUE && v >= Integer.MIN_VALUE) {
+            return toString((int) v);
+        }
+        return Long.toString(v);
+    }
+
+    public static String toString(double v) {
+        return Double.toString(v);
+    }
+
+    /**
+     * @since 2.6.0
+     */
+    public static String toString(float v) {
+        return Float.toString(v);
+    }
+
+    /*
+    /**********************************************************
+    /* Internal helper methods
+    /**********************************************************
+     */
+
+    private static int _outputUptoBillion(int v, char[] b, int off)
+    {
+        if (v < MILLION) { // at most 2 triplets...
+            if (v < 1000) {
+                return _leading3(v, b, off);
+            }
+            int thousands = v / 1000;
+            int ones = v - (thousands * 1000); // == value % 1000
+            return _outputUptoMillion(b, off, thousands, ones);
+        }
+        int thousands = v / 1000;
+        int ones = (v - (thousands * 1000)); // == value % 1000
+        int millions = thousands / 1000;
+        thousands -= (millions * 1000);
+
+        off = _leading3(millions, b, off);
+
+        int enc = TRIPLET_TO_CHARS[thousands];
+        b[off++] = (char) (enc >> 16);
+        b[off++] = (char) ((enc >> 8) & 0x7F);
+        b[off++] = (char) (enc & 0x7F);
+
+        enc = TRIPLET_TO_CHARS[ones];
+        b[off++] = (char) (enc >> 16);
+        b[off++] = (char) ((enc >> 8) & 0x7F);
+        b[off++] = (char) (enc & 0x7F);
+
+        return off;
+    }
+
+    private static int _outputFullBillion(int v, char[] b, int off)
+    {
+        int thousands = v / 1000;
+        int ones = (v - (thousands * 1000)); // == value % 1000
+        int millions = thousands / 1000;
+
+        int enc = TRIPLET_TO_CHARS[millions];
+        b[off++] = (char) (enc >> 16);
+        b[off++] = (char) ((enc >> 8) & 0x7F);
+        b[off++] = (char) (enc & 0x7F);
+
+        thousands -= (millions * 1000);
+        enc = TRIPLET_TO_CHARS[thousands];
+        b[off++] = (char) (enc >> 16);
+        b[off++] = (char) ((enc >> 8) & 0x7F);
+        b[off++] = (char) (enc & 0x7F);
+
+        enc = TRIPLET_TO_CHARS[ones];
+        b[off++] = (char) (enc >> 16);
+        b[off++] = (char) ((enc >> 8) & 0x7F);
+        b[off++] = (char) (enc & 0x7F);
+
+        return off;
+    }
+
+    private static int _outputUptoBillion(int v, byte[] b, int off)
+    {
+        if (v < MILLION) { // at most 2 triplets...
+            if (v < 1000) {
+                return _leading3(v, b, off);
+            }
+            int thousands = v / 1000;
+            int ones = v - (thousands * 1000); // == value % 1000
+            return _outputUptoMillion(b, off, thousands, ones);
+        }
+        int thousands = v / 1000;
+        int ones = (v - (thousands * 1000)); // == value % 1000
+        int millions = thousands / 1000;
+        thousands -= (millions * 1000);
+
+        off = _leading3(millions, b, off);
+
+        int enc = TRIPLET_TO_CHARS[thousands];
+        b[off++] = (byte) (enc >> 16);
+        b[off++] = (byte) (enc >> 8);
+        b[off++] = (byte) enc;
+
+        enc = TRIPLET_TO_CHARS[ones];
+        b[off++] = (byte) (enc >> 16);
+        b[off++] = (byte) (enc >> 8);
+        b[off++] = (byte) enc;
+
+        return off;
+    }
+
+    private static int _outputFullBillion(int v, byte[] b, int off)
+    {
+        int thousands = v / 1000;
+        int ones = (v - (thousands * 1000)); // == value % 1000
+        int millions = thousands / 1000;
+        thousands -= (millions * 1000);
+
+        int enc = TRIPLET_TO_CHARS[millions];
+        b[off++] = (byte) (enc >> 16);
+        b[off++] = (byte) (enc >> 8);
+        b[off++] = (byte) enc;
+
+        enc = TRIPLET_TO_CHARS[thousands];
+        b[off++] = (byte) (enc >> 16);
+        b[off++] = (byte) (enc >> 8);
+        b[off++] = (byte) enc;
+
+        enc = TRIPLET_TO_CHARS[ones];
+        b[off++] = (byte) (enc >> 16);
+        b[off++] = (byte) (enc >> 8);
+        b[off++] = (byte) enc;
+
+        return off;
+    }
+
+    private static int _outputUptoMillion(char[] b, int off, int thousands, 
int ones)
+    {
+        int enc = TRIPLET_TO_CHARS[thousands];
+        if (thousands > 9) {
+            if (thousands > 99) {
+                b[off++] = (char) (enc >> 16);
+            }
+            b[off++] = (char) ((enc >> 8) & 0x7F);
+        }
+        b[off++] = (char) (enc & 0x7F);
+        // and then full
+        enc = TRIPLET_TO_CHARS[ones];
+        b[off++] = (char) (enc >> 16);
+        b[off++] = (char) ((enc >> 8) & 0x7F);
+        b[off++] = (char) (enc & 0x7F);
+        return off;
+    }
+
+    private static int _outputUptoMillion(byte[] b, int off, int thousands, 
int ones)
+    {
+        int enc = TRIPLET_TO_CHARS[thousands];
+        if (thousands > 9) {
+            if (thousands > 99) {
+                b[off++] = (byte) (enc >> 16);
+            }
+            b[off++] = (byte) (enc >> 8);
+        }
+        b[off++] = (byte) enc;
+        // and then full
+        enc = TRIPLET_TO_CHARS[ones];
+        b[off++] = (byte) (enc >> 16);
+        b[off++] = (byte) (enc >> 8);
+        b[off++] = (byte) enc;
+        return off;
+    }
+
+    private static int _leading3(int t, char[] b, int off)
+    {
+        int enc = TRIPLET_TO_CHARS[t];
+        if (t > 9) {
+            if (t > 99) {
+                b[off++] = (char) (enc >> 16);
+            }
+            b[off++] = (char) ((enc >> 8) & 0x7F);
+        }
+        b[off++] = (char) (enc & 0x7F);
+        return off;
+    }
+
+    private static int _leading3(int t, byte[] b, int off)
+    {
+        int enc = TRIPLET_TO_CHARS[t];
+        if (t > 9) {
+            if (t > 99) {
+                b[off++] = (byte) (enc >> 16);
+            }
+            b[off++] = (byte) (enc >> 8);
+        }
+        b[off++] = (byte) enc;
+        return off;
+    }
+
+    private static int _full3(int t, char[] b, int off)
+    {
+        int enc = TRIPLET_TO_CHARS[t];
+        b[off++] = (char) (enc >> 16);
+        b[off++] = (char) ((enc >> 8) & 0x7F);
+        b[off++] = (char) (enc & 0x7F);
+        return off;
+    }
+
+    private static int _full3(int t, byte[] b, int off)
+    {
+        int enc = TRIPLET_TO_CHARS[t];
+        b[off++] = (byte) (enc >> 16);
+        b[off++] = (byte) (enc >> 8);
+        b[off++] = (byte) enc;
+        return off;
+    }
+
+    // // // Special cases for where we can not flip the sign bit
+
+    private static int _outputSmallestL(char[] b, int off)
+    {
+        int len = SMALLEST_LONG.length();
+        SMALLEST_LONG.getChars(0, len, b, off);
+        return (off + len);
+    }
+
+    private static int _outputSmallestL(byte[] b, int off)
+    {
+        int len = SMALLEST_LONG.length();
+        for (int i = 0; i < len; ++i) {
+            b[off++] = (byte) SMALLEST_LONG.charAt(i);
+        }
+        return off;
+    }
+
+    private static int _outputSmallestI(char[] b, int off)
+    {
+        int len = SMALLEST_INT.length();
+        SMALLEST_INT.getChars(0, len, b, off);
+        return (off + len);
+    }
+
+    private static int _outputSmallestI(byte[] b, int off)
+    {
+        int len = SMALLEST_INT.length();
+        for (int i = 0; i < len; ++i) {
+            b[off++] = (byte) SMALLEST_INT.charAt(i);
+        }
+        return off;
+    }
+}
diff --git a/audit/src/test/java/org/apache/karaf/audit/MapEvent.java 
b/audit/src/test/java/org/apache/karaf/audit/MapEvent.java
new file mode 100644
index 0000000..92bc8cb
--- /dev/null
+++ b/audit/src/test/java/org/apache/karaf/audit/MapEvent.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit;
+
+import javax.security.auth.Subject;
+import java.util.Map;
+
+public class MapEvent implements Event {
+
+    final long timestamp;
+    final Map<String, Object> map;
+
+    public MapEvent(Map<String, Object> map) {
+        this.map = map;
+        this.timestamp = System.currentTimeMillis();
+    }
+
+    public MapEvent(Map<String, Object> map, long timestamp) {
+        this.map = map;
+        this.timestamp = timestamp;
+    }
+
+    @Override
+    public long timestamp() {
+        return timestamp;
+    }
+
+    @Override
+    public Subject subject() {
+        return null;
+    }
+
+    @Override
+    public String type() {
+        return (String) map.get("type");
+    }
+
+    @Override
+    public String subtype() {
+        return (String) map.get("subtype");
+    }
+
+    @Override
+    public Iterable<String> keys() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getProperty(String key) {
+        return map.get(key);
+    }
+}
diff --git a/audit/src/test/java/org/apache/karaf/audit/TestPerf.java 
b/audit/src/test/java/org/apache/karaf/audit/TestPerf.java
new file mode 100644
index 0000000..c2e1efe
--- /dev/null
+++ b/audit/src/test/java/org/apache/karaf/audit/TestPerf.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit;
+
+import org.apache.karaf.audit.layout.Rfc3164Layout;
+import org.apache.karaf.audit.layout.Rfc5424Layout;
+import org.apache.karaf.audit.util.Buffer;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javax.management.ObjectName;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+@Ignore
+public class TestPerf {
+
+    private static final String INVOKE = "invoke";
+    private static final String[] INVOKE_SIG = new String[] 
{ObjectName.class.getName(), String.class.getName(), Object[].class.getName(), 
String[].class.getName()};
+
+
+    @Test
+    public void testFormatString() throws Exception {
+        int iterations = 10000000;
+
+        for (int i = 0; i < 10; i++) {
+            final Buffer buffer0 = new Buffer(Buffer.Format.Json);
+            long t0 = measure(() -> {
+                buffer0.clear();
+                buffer0.format("This is 
\"\n\tquite\n\tq\n\ta\n\tlong\n\tquote.\"\nIndeed !\n");
+                return null;
+            }, iterations);
+            System.out.println("json = " + t0);
+
+            final Buffer buffer1 = new Buffer(Buffer.Format.Syslog);
+            long t1 = measure(() -> {
+                buffer1.clear();
+                buffer1.format("This is 
\"\n\tquite\n\tq\n\ta\n\tlong\n\tquote.\"\nIndeed !\n");
+                return null;
+            }, iterations);
+            System.out.println("syslog = " + t1);
+        }
+    }
+
+    @Test
+    public void testGelfTimestamp() throws Exception {
+        long timestamp = System.currentTimeMillis();
+
+        int iterations = 1000000;
+
+        for (int p = 0; p < 10; p++) {
+            Buffer buffer = new Buffer(Buffer.Format.Json);
+            long t0 = measure(() -> {
+                buffer.clear();
+                long secs = timestamp / 1000;
+                int ms = (int) (timestamp - secs * 1000);
+                buffer.format(secs);
+                buffer.append('.');
+                int temp = ms / 100;
+                buffer.append((char) (temp + '0'));
+                ms -= 100 * temp;
+                temp = ms / 10;
+                buffer.append((char) (temp + '0'));
+                ms -= 10 * temp;
+                buffer.append((char) (ms + '0'));
+                return null;
+            }, iterations);
+            System.out.println("t0 = " + t0);
+            long t1 = measure(() -> {
+                buffer.clear();
+                long secs = timestamp / 1000;
+                int ms = (int) (timestamp - secs * 1000);
+                buffer.format(Long.toString(secs));
+                buffer.append('.');
+                int temp = ms / 100;
+                buffer.append((char) (temp + '0'));
+                ms -= 100 * temp;
+                temp = ms / 10;
+                buffer.append((char) (temp + '0'));
+                ms -= 10 * temp;
+                buffer.append((char) (ms + '0'));
+                return null;
+            }, iterations);
+            System.out.println("t1 = " + t1);
+            long t2 = measure(() -> {
+                buffer.clear();
+                new Formatter(buffer).format("%.3f", ((double) timestamp) / 
1000.0);
+                return null;
+            }, iterations);
+            System.out.println("t2 = " + t2);
+        }
+    }
+
+    @Test
+    public void testSerialize() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", Event.TYPE_JMX);
+        map.put("subtype", INVOKE);
+        map.put("method", INVOKE);
+        map.put("signature", INVOKE_SIG);
+        map.put("params", new Object[] { new 
ObjectName("org.apache.karaf.Mbean:type=foo"), "myMethod", new Object[] { 
String.class.getName() }, new String[] { "the-param "}});
+        Event event = new MapEvent(map);
+
+        EventLayout layout = new Rfc3164Layout(16, 5, 
Rfc5424Layout.DEFAULT_ENTERPRISE_NUMBER);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        Writer writer = new BufferedWriter(new OutputStreamWriter(baos, 
StandardCharsets.UTF_8));
+
+        long dt0 = measure(() -> { baos.reset(); layout.format(event, writer); 
writer.flush(); return null; }, 10000000);
+        System.out.println(dt0);
+
+        long dt1 = measure(() -> { baos.reset(); layout.format(event, writer); 
writer.flush(); return null; }, 10000000);
+        System.out.println(dt1);
+    }
+
+    private <T> long measure(Callable<T> runnable, int runs) throws Exception {
+        System.gc();
+        for (int i = 0; i < runs / 100; i++) {
+            runnable.call();
+        }
+        System.gc();
+        long t0 = System.currentTimeMillis();
+        for (int i = 0; i < runs; i++) {
+            runnable.call();
+        }
+        long t1 = System.currentTimeMillis();
+        return t1 - t0;
+    }
+
+}
diff --git 
a/audit/src/test/java/org/apache/karaf/audit/logger/EventLoggerTest.java 
b/audit/src/test/java/org/apache/karaf/audit/logger/EventLoggerTest.java
new file mode 100644
index 0000000..f33d88f
--- /dev/null
+++ b/audit/src/test/java/org/apache/karaf/audit/logger/EventLoggerTest.java
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.logger;
+
+import org.apache.karaf.audit.Event;
+import org.apache.karaf.audit.EventLayout;
+import org.apache.karaf.audit.EventLogger;
+import org.apache.karaf.audit.MapEvent;
+import org.apache.karaf.audit.layout.GelfLayout;
+import org.apache.karaf.audit.layout.Rfc3164Layout;
+import org.apache.karaf.audit.layout.Rfc5424Layout;
+import org.junit.Test;
+
+import javax.management.ObjectName;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class EventLoggerTest {
+
+    private static final String INVOKE = "invoke";
+    private static final String[] INVOKE_SIG = new String[] 
{ObjectName.class.getName(), String.class.getName(), Object[].class.getName(), 
String[].class.getName()};
+
+    @Test
+    public void testUdp() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", Event.TYPE_JMX);
+        map.put("subtype", INVOKE);
+        map.put("method", INVOKE);
+        map.put("signature", INVOKE_SIG);
+        map.put("params", new Object[] { new 
ObjectName("org.apache.karaf.Mbean:type=foo"), "myMethod", new Object[] { 
String.class.getName() }, new String[] { "the-param "}});
+        Event event = new MapEvent(map, 1510902000000L);
+
+        int port = getNewPort();
+
+        DatagramSocket socket = new DatagramSocket(port);
+        List<DatagramPacket> packets = new ArrayList<>();
+        new Thread(() -> {
+            try {
+                DatagramPacket dp = new DatagramPacket(new byte[1024], 1024);
+                socket.receive(dp);
+                packets.add(dp);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+
+        }).start();
+
+        Thread.sleep(100);
+
+        EventLayout layout = new Rfc3164Layout(16, 5, 
Rfc5424Layout.DEFAULT_ENTERPRISE_NUMBER);
+        EventLogger logger = new UdpEventLogger("localhost", port, "UTF-8", 
layout);
+        logger.write(event);
+
+        Thread.sleep(100);
+
+        assertEquals(1, packets.size());
+        DatagramPacket p = packets.get(0);
+        String str = new String(p.getData(), 0, p.getLength(), 
StandardCharsets.UTF_8);
+        assertTrue(str.startsWith("<133>Nov 17 08:00:00 "));
+        assertTrue(str.endsWith(" jmx [jmx@18060 type=\"jmx\" 
subtype=\"invoke\" method=\"invoke\" signature=\"[javax.management.ObjectName, 
java.lang.String, [Ljava.lang.Object;, [Ljava.lang.String;\\]\" 
params=\"[org.apache.karaf.Mbean:type=foo, myMethod, [java.lang.String\\], 
[the-param \\]\\]\"]"));
+        System.out.println(str);
+    }
+
+    @Test
+    public void testTcp() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", Event.TYPE_JMX);
+        map.put("subtype", INVOKE);
+        map.put("method", INVOKE);
+        map.put("signature", INVOKE_SIG);
+        map.put("params", new Object[] { new 
ObjectName("org.apache.karaf.Mbean:type=foo"), "myMethod", new Object[] { 
String.class.getName() }, new String[] { "the-param "}});
+        Event event = new MapEvent(map, 1510902000000L);
+
+        int port = getNewPort();
+
+        List<String> packets = new ArrayList<>();
+        new Thread(() -> {
+            try (ServerSocket ssocket = new ServerSocket(port)) {
+                ssocket.setReuseAddress(true);
+                try (Socket socket = ssocket.accept()) {
+                    byte[] buffer = new byte[1024];
+                    int nb = socket.getInputStream().read(buffer);
+                    packets.add(new String(buffer, 0, nb, 
StandardCharsets.UTF_8));
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+
+        }).start();
+
+        Thread.sleep(100);
+
+        EventLayout layout = new Rfc5424Layout(16, 5, 
Rfc5424Layout.DEFAULT_ENTERPRISE_NUMBER);
+        EventLogger logger = new TcpEventLogger("localhost", port, "UTF-8", 
layout);
+        logger.write(event);
+        logger.flush();
+
+        Thread.sleep(100);
+
+        assertEquals(1, packets.size());
+        String str = packets.get(0);
+        System.out.println(str);
+        assertTrue(str.startsWith("<133>1 2017-11-17T08:00:00.000+01:00 "));
+        assertTrue(str.indexOf(" jmx [jmx@18060 type=\"jmx\" 
subtype=\"invoke\" method=\"invoke\" signature=\"[javax.management.ObjectName, 
java.lang.String, [Ljava.lang.Object;, [Ljava.lang.String;\\]\" 
params=\"[org.apache.karaf.Mbean:type=foo, myMethod, [java.lang.String\\], 
[the-param \\]\\]\"]") > 0);
+    }
+
+    @Test
+    public void testFile() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", Event.TYPE_JMX);
+        map.put("subtype", INVOKE);
+        map.put("method", INVOKE);
+        map.put("signature", INVOKE_SIG);
+        map.put("params", new Object[] { new 
ObjectName("org.apache.karaf.Mbean:type=foo"), "myMethod", new Object[] { 
String.class.getName() }, new String[] { "the-param "}});
+
+        EventLayout layout = new GelfLayout();
+        Path path = Files.createTempDirectory("file-logger");
+        String file = path.resolve("file.log").toString();
+        EventLogger logger = new FileEventLogger(file, "UTF-8", "daily", 2, 
false, Executors.defaultThreadFactory(), layout);
+
+        logger.write(new MapEvent(map, 1510902000000L));
+        logger.write(new MapEvent(map, 1510984800000L));
+        logger.close();
+
+        Thread.sleep(100);
+
+        List<Path> paths = 
Files.list(path).sorted().collect(Collectors.toList());
+        Collections.sort(paths);
+        assertEquals(2, paths.size());
+        assertEquals("file-2017-11-18.log", 
paths.get(0).getFileName().toString());
+        assertEquals("file.log", paths.get(1).getFileName().toString());
+
+        List<String> lines = Files.readAllLines(paths.get(0), 
StandardCharsets.UTF_8);
+        assertEquals(1, lines.size());
+        String str = lines.get(0);
+        System.out.println(str);
+        assertTrue(str.startsWith("{ version=\"1.1\" host=\""));
+        assertTrue(str.endsWith("timestamp=1510902000.000 
short_message=\"jmx.invoke\" _type=\"jmx\" _subtype=\"invoke\" 
_method=\"invoke\" _signature=\"[javax.management.ObjectName, java.lang.String, 
[Ljava.lang.Object;, [Ljava.lang.String;]\" 
_params=\"[org.apache.karaf.Mbean:type=foo, myMethod, [java.lang.String], 
[the-param ]]\" }"));
+
+        lines = Files.readAllLines(paths.get(1), StandardCharsets.UTF_8);
+        assertEquals(1, lines.size());
+        str = lines.get(0);
+        System.out.println(str);
+        assertTrue(str.startsWith("{ version=\"1.1\" host=\""));
+        assertTrue(str.endsWith("timestamp=1510984800.000 
short_message=\"jmx.invoke\" _type=\"jmx\" _subtype=\"invoke\" 
_method=\"invoke\" _signature=\"[javax.management.ObjectName, java.lang.String, 
[Ljava.lang.Object;, [Ljava.lang.String;]\" 
_params=\"[org.apache.karaf.Mbean:type=foo, myMethod, [java.lang.String], 
[the-param ]]\" }"));
+    }
+
+    @Test
+    public void testFileMaxFiles() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", Event.TYPE_SHELL);
+        map.put("subtype", "executed");
+        map.put("script", "a-script");
+
+        EventLayout layout = new GelfLayout();
+        Path path = Files.createTempDirectory("file-logger");
+        String file = path.resolve("file.log").toString();
+        EventLogger logger = new FileEventLogger(file, "UTF-8", "daily", 2, 
false, Executors.defaultThreadFactory(), layout);
+
+        for (int i = 0; i < 10; i++) {
+            logger.write(new MapEvent(map, 1510902000000L + 
TimeUnit.DAYS.toMillis(i)));
+        }
+        logger.close();
+
+        Thread.sleep(100);
+
+        List<String> paths = Files.list(path)
+                .map(Path::getFileName).map(Path::toString)
+                .sorted().collect(Collectors.toList());
+        assertEquals(Arrays.asList("file-2017-11-25.log", 
"file-2017-11-26.log", "file.log"), paths);
+    }
+
+    @Test
+    public void testFileSize() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", Event.TYPE_SHELL);
+        map.put("subtype", "executed");
+        map.put("script", "a-script");
+
+        EventLayout layout = new GelfLayout();
+        Path path = Files.createTempDirectory("file-logger");
+        String file = path.resolve("file.log").toString();
+        EventLogger logger = new FileEventLogger(file, "UTF-8", "size(10)", 2, 
false, Executors.defaultThreadFactory(), layout);
+
+        for (int i = 0; i < 10; i++) {
+            logger.write(new MapEvent(map, 1510902000000L + 
TimeUnit.HOURS.toMillis(i)));
+        }
+        logger.close();
+
+        Thread.sleep(100);
+
+        List<String> paths = Files.list(path)
+                .map(Path::getFileName).map(Path::toString)
+                .sorted().collect(Collectors.toList());
+        assertEquals(Arrays.asList("file-2017-11-17-2.log", 
"file-2017-11-17.log", "file.log"), paths);
+    }
+
+    @Test
+    public void testFileSizeCompress() throws Exception {
+        Map<String, Object> map = new HashMap<>();
+        map.put("type", Event.TYPE_SHELL);
+        map.put("subtype", "executed");
+        map.put("script", "a-script");
+
+        EventLayout layout = new GelfLayout();
+        Path path = Files.createTempDirectory("file-logger");
+        String file = path.resolve("file.log").toString();
+        EventLogger logger = new FileEventLogger(file, "UTF-8", "size(10)", 2, 
true, Executors.defaultThreadFactory(), layout);
+
+        for (int i = 0; i < 10; i++) {
+            logger.write(new MapEvent(map, 1510902000000L + 
TimeUnit.HOURS.toMillis(i)));
+        }
+        logger.close();
+
+        Thread.sleep(100);
+
+        List<String> paths = Files.list(path)
+                .map(Path::getFileName).map(Path::toString)
+                .sorted().collect(Collectors.toList());
+        assertEquals(Arrays.asList("file-2017-11-17-2.log.gz", 
"file-2017-11-17.log.gz", "file.log"), paths);
+    }
+
+    private int getNewPort() throws IOException {
+        try (ServerSocket socket = new ServerSocket()) {
+            socket.setReuseAddress(true);
+            socket.bind(new InetSocketAddress("localhost", 0));
+            return socket.getLocalPort();
+        }
+    }
+
+}
diff --git 
a/audit/src/test/java/org/apache/karaf/audit/util/FastDateFormatTest.java 
b/audit/src/test/java/org/apache/karaf/audit/util/FastDateFormatTest.java
new file mode 100644
index 0000000..c1f4ec5
--- /dev/null
+++ b/audit/src/test/java/org/apache/karaf/audit/util/FastDateFormatTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.karaf.audit.util;
+
+import org.junit.Test;
+
+import java.text.SimpleDateFormat;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+
+public class FastDateFormatTest {
+
+    @Test
+    public void test() throws Exception {
+        FastDateFormat cal = new FastDateFormat();
+
+        long time = new 
SimpleDateFormat("yyyy-MM-dd").parse("2017-11-05").getTime();
+        assertEquals("Nov  5", cal.getDate(time, FastDateFormat.MMM_D2));
+        assertEquals("2017-11-05", cal.getDate(time, 
FastDateFormat.YYYY_MM_DD));
+
+        time += TimeUnit.DAYS.toMillis(5);
+        assertEquals("Nov 10", cal.getDate(time, FastDateFormat.MMM_D2));
+        assertEquals("2017-11-10", cal.getDate(time, 
FastDateFormat.YYYY_MM_DD));
+
+
+    }
+}
diff --git a/pom.xml b/pom.xml
index 215eb11..f09198e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -66,6 +66,7 @@
         <module>jpa</module>
         <module>maven</module>
         <module>services</module>
+        <module>audit</module>
         <module>subsystem</module>
         <module>profile</module>
         <module>event</module>
diff --git 
a/services/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/Configuration.java
 
b/services/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/Configuration.java
index 86631dd..2bf1d6a 100644
--- 
a/services/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/Configuration.java
+++ 
b/services/eventadmin/src/main/java/org/apache/felix/eventadmin/impl/Configuration.java
@@ -451,11 +451,12 @@ public class Configuration
      */
     private void adaptEvents(final EventAdmin admin)
     {
-        m_adapters = new AbstractAdapter[4];
+        m_adapters = new AbstractAdapter[3];
         m_adapters[0] = new FrameworkEventAdapter(m_bundleContext, admin);
         m_adapters[1] = new BundleEventAdapter(m_bundleContext, admin);
         m_adapters[2] = new ServiceEventAdapter(m_bundleContext, admin);
-        m_adapters[3] = new LogEventAdapter(m_bundleContext, admin);
+        // KARAF: disable log events as they are published by PaxLogging
+        //m_adapters[3] = new LogEventAdapter(m_bundleContext, admin);
     }
 
     private Object tryToCreateMetaTypeProvider(final Object managedService)
diff --git 
a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
 
b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
index e641f8f..713b3ca 100644
--- 
a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
+++ 
b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
@@ -76,6 +76,7 @@ import org.jline.reader.SyntaxError;
 import org.jline.reader.UserInterruptException;
 import org.jline.terminal.Terminal.Signal;
 import org.jline.terminal.impl.DumbTerminal;
+import org.osgi.service.event.EventAdmin;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -555,6 +556,7 @@ public class ConsoleSessionImpl implements Session {
     }
 
     private void doExecuteScript(Path scriptFileName) {
+        Object oldScript = session.put("script", 
Paths.get(System.getProperty("karaf.home")).relativize(scriptFileName));
         try {
             String script = String.join("\n",
                     Files.readAllLines(scriptFileName));
@@ -562,6 +564,8 @@ public class ConsoleSessionImpl implements Session {
         } catch (Exception e) {
             LOGGER.debug("Error in initialization script {}", scriptFileName, 
e);
             System.err.println("Error in initialization script: " + 
scriptFileName + ": " + e.getMessage());
+        } finally {
+            session.put("script", oldScript);
         }
     }
 
diff --git 
a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/EventAdminListener.java
 
b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/EventAdminListener.java
index ec249a2..0e7fd06 100644
--- 
a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/EventAdminListener.java
+++ 
b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/osgi/EventAdminListener.java
@@ -48,28 +48,31 @@ public class EventAdminListener implements 
CommandSessionListener, Closeable
     }
 
     public void afterExecute(CommandSession session, CharSequence command, 
Exception exception) {
-        sendEvent(command, null, exception);
+        sendEvent(session, command, null, exception);
     }
 
     public void afterExecute(CommandSession session, CharSequence command, 
Object result) {
-        sendEvent(command, result, null);
+        sendEvent(session, command, result, null);
     }
 
-    private void sendEvent(CharSequence command, Object result, Exception 
exception) {
-        if (command.toString().trim().length() > 0) {
-            EventAdmin admin = tracker.getService();
-            if (admin != null) {
-                Map<String, Object> props = new HashMap<>();
+    private void sendEvent(CommandSession session, CharSequence command, 
Object result, Exception exception) {
+        EventAdmin admin = tracker.getService();
+        if (admin != null) {
+            Map<String, Object> props = new HashMap<>();
+            Object script = session.get("script");
+            if (script != null) {
+                props.put("script", script.toString());
+            } else if (command.toString().trim().length() > 0) {
                 props.put("command", command.toString());
-                if (result != null) {
-                    props.put("result", result);
-                }
-                if (exception != null) {
-                    props.put("exception", exception);
-                }
-                Event event = new 
Event("org/apache/karaf/shell/console/EXECUTED", props);
-                admin.postEvent(event);
             }
+            if (result != null) {
+                props.put("result", result);
+            }
+            if (exception != null) {
+                props.put("exception", exception);
+            }
+            Event event = new Event("org/apache/karaf/shell/console/EXECUTED", 
props);
+            admin.postEvent(event);
         }
     }
 

-- 
To stop receiving notification emails like this one, please contact
"[email protected]" <[email protected]>.

Reply via email to