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]>.
