backporting TOMEE-1631 - Rotating JUL Handler
Project: http://git-wip-us.apache.org/repos/asf/tomee/repo Commit: http://git-wip-us.apache.org/repos/asf/tomee/commit/da4df975 Tree: http://git-wip-us.apache.org/repos/asf/tomee/tree/da4df975 Diff: http://git-wip-us.apache.org/repos/asf/tomee/diff/da4df975 Branch: refs/heads/tomee-1.7.x Commit: da4df975e5caf6d022788c6f9fe605c2cc197d4e Parents: 8afa54e Author: Romain Manni-Bucau <rmann...@gmail.com> Authored: Mon Sep 28 10:48:52 2015 -0700 Committer: Romain Manni-Bucau <rmann...@gmail.com> Committed: Mon Sep 28 10:48:52 2015 -0700 ---------------------------------------------------------------------- tomee/tomee-juli/pom.xml | 27 + .../org/apache/juli/logging/LogFactory.java | 133 ----- .../org/apache/tomee/TomEELogConfigurer.java | 96 --- .../jul/formatter/AsyncConsoleHandler.java | 34 +- .../tomee/jul/formatter/log/JULLogger.java | 179 ++++++ .../tomee/jul/formatter/log/LoggerFactory.java | 23 + .../tomee/jul/formatter/log/NoopLogger.java | 117 ++++ .../tomee/jul/formatter/log/ReloadableLog.java | 111 ++++ .../tomee/jul/formatter/log/TomEELog.java | 164 +++++ .../handler/rotating/BackgroundTaskRunner.java | 91 +++ .../tomee/jul/handler/rotating/Duration.java | 178 ++++++ .../jul/handler/rotating/LocalFileHandler.java | 593 +++++++++++++++++++ .../apache/tomee/jul/handler/rotating/Size.java | 343 +++++++++++ .../jul/handler/rotating/ArchivingTest.java | 215 +++++++ .../handler/rotating/LocalFileHandlerTest.java | 128 ++++ .../tomee/jul/handler/rotating/PerfRunner.java | 109 ++++ 16 files changed, 2311 insertions(+), 230 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/pom.xml ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/pom.xml b/tomee/tomee-juli/pom.xml index 3c57d80..bc578f8 100644 --- a/tomee/tomee-juli/pom.xml +++ b/tomee/tomee-juli/pom.xml @@ -45,8 +45,35 @@ <artifactId>slf4j-api</artifactId> <scope>provided</scope> </dependency> + + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.openjdk.jmh</groupId> + <artifactId>jmh-core</artifactId> + <version>${jmh.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.openjdk.jmh</groupId> + <artifactId>jmh-generator-annprocess</artifactId> + <version>${jmh.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <scope>test</scope> + </dependency> </dependencies> + <properties> + <jmh.version>1.10.5</jmh.version> + </properties> + <build> <plugins> <plugin> http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/juli/logging/LogFactory.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/juli/logging/LogFactory.java b/tomee/tomee-juli/src/main/java/org/apache/juli/logging/LogFactory.java deleted file mode 100644 index 019b45f..0000000 --- a/tomee/tomee-juli/src/main/java/org/apache/juli/logging/LogFactory.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * 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.juli.logging; - - -import java.util.Collection; -import java.util.HashSet; -import java.util.Properties; -import java.util.logging.LogManager; - - -// tomcat doesn't have any spi mecanism so forking this class -public /* abstract */ class LogFactory { - public static final String FACTORY_PROPERTY = - "org.apache.commons.logging.LogFactory"; - - public static final String FACTORY_DEFAULT = - "org.apache.commons.logging.impl.LogFactoryImpl"; - - public static final String FACTORY_PROPERTIES = - "commons-logging.properties"; - - public static final String HASHTABLE_IMPLEMENTATION_PROPERTY = - "org.apache.commons.logging.LogFactory.HashtableImpl"; - - private static LogFactory singleton=new LogFactory(); - private final Collection<String> names = new HashSet<String>(); - - Properties logConfig; - - protected LogFactory() { - logConfig=new Properties(); - } - - protected static void setSingleton(final LogFactory singleton) { - if (singleton == null) { - return; - } - LogFactory.singleton = singleton; - } - - void setLogConfig( final Properties p ) { - this.logConfig=p; - } - - public synchronized Collection<String> getNames() { - return names; - } - - public synchronized Log getInstance(final String name) throws LogConfigurationException { - names.add(name); - return DirectJDKLog.getInstance(name); - } - - public void release() { - DirectJDKLog.release(); - } - - public Object getAttribute(final String name) { - return logConfig.get(name); - } - - public String[] getAttributeNames() { - final String[] result = new String[logConfig.size()]; - return logConfig.keySet().toArray(result); - } - - public void removeAttribute(final String name) { - logConfig.remove(name); - } - - public void setAttribute(final String name, final Object value) { - logConfig.put(name, value); - } - - public Log getInstance(final Class<?> clazz) - throws LogConfigurationException { - return getInstance( clazz.getName()); - } - - public static LogFactory getFactory() throws LogConfigurationException { - return singleton; - } - - public static Log getLog(final Class<?> clazz) - throws LogConfigurationException { - return getFactory().getInstance(clazz); - - } - - public static Log getLog(final String name) - throws LogConfigurationException { - return getFactory().getInstance(name); - - } - - public static void release(final ClassLoader classLoader) { - // JULI's log manager looks at the current classLoader so there is no - // need to use the passed in classLoader, the default implementation - // does not so calling reset in that case will break things - if (!LogManager.getLogManager().getClass().getName().equals( - "java.util.logging.LogManager")) { - LogManager.getLogManager().reset(); - } - } - - public static void releaseAll() { - singleton.release(); - } - - public static String objectId(final Object o) { - if (o == null) { - return "null"; - } else { - return o.getClass().getName() + "@" + System.identityHashCode(o); - } - } -} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/TomEELogConfigurer.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/TomEELogConfigurer.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/TomEELogConfigurer.java deleted file mode 100644 index e62ac08..0000000 --- a/tomee/tomee-juli/src/main/java/org/apache/tomee/TomEELogConfigurer.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.tomee; - -import org.apache.juli.logging.Log; -import org.apache.juli.logging.LogFactory; - -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; - -public abstract class TomEELogConfigurer extends LogFactory { - public static void configureLogs() { - if (Boolean.getBoolean("tomee.skip-tomcat-log")) { - return; - } - - final Thread thread = Thread.currentThread(); - try { - final ClassLoader tccl = thread.getContextClassLoader(); // this is in classpath not StandardClassLoader so use reflection - final Class<?> logger = tccl.loadClass("org.apache.openejb.util.Logger"); - final Method m = logger.getDeclaredMethod("delegateClass"); - final String clazz = (String) m.invoke(null); - final LogFactory factory; - if ("org.apache.openejb.util.Log4jLogStreamFactory".equals(clazz)) { - factory = LogFactory.class.cast(tccl.loadClass("org.apache.tomee.loader.log.Log4jLogFactory").newInstance()); - } else if ("org.apache.openejb.util.Slf4jLogStreamFactory".equals(clazz)) { - factory = LogFactory.class.cast(tccl.loadClass("org.apache.tomee.loader.log.Slf4jLogFactory").newInstance()); - } else { - factory = null; - } - if (factory != null) { - final LogFactory oldFactory = getFactory(); - final Collection<String> names = new ArrayList<String>(oldFactory.getNames()); - oldFactory.getNames().clear(); - oldFactory.release(); - setSingleton(factory); - reload(factory, tccl, names); - } - } catch (final Throwable th) { - System.err.println(th.getClass().getName() + ": " + th.getMessage()); - } - } - - private static void reload(final LogFactory factory, final ClassLoader tccl, final Collection<String> names) { - for (final String name : names) { - try { - final Field f = Class.forName(name, false, tccl).getDeclaredField("log"); - if (!Log.class.equals(f.getType())) { - continue; - } - - final boolean acc = f.isAccessible(); - f.setAccessible(true); - - final Log newValue = factory.getInstance(name); - final int modifiers = f.getModifiers(); - if (Modifier.isFinal(modifiers)) { - final Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(f, modifiers & ~Modifier.FINAL); - - f.set(null, newValue); - - modifiersField.setInt(f, modifiers & Modifier.FINAL); - } else { - f.set(null, newValue); - } - - f.setAccessible(acc); - } catch (final Throwable e) { - // no-op - } - } - } - - private TomEELogConfigurer() { - // no-op - } -} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/AsyncConsoleHandler.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/AsyncConsoleHandler.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/AsyncConsoleHandler.java index 4eeb1e8..c6724c0 100644 --- a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/AsyncConsoleHandler.java +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/AsyncConsoleHandler.java @@ -18,13 +18,45 @@ package org.apache.tomee.jul.formatter; import org.apache.juli.AsyncFileHandler; +import java.io.PrintWriter; +import java.io.StringWriter; import java.util.logging.ConsoleHandler; +import java.util.logging.Formatter; import java.util.logging.LogRecord; public class AsyncConsoleHandler extends AsyncFileHandler { - private final ConsoleHandler delegate = new ConsoleHandler(); + private final ConsoleHandler delegate = new ConsoleHandler() {{ + setFormatter(new SingleLineFormatter()); // console -> dev. File uses plain old format + }}; protected void publishInternal(final LogRecord record) { delegate.publish(record); } + + // copy cause of classloading + private static class SingleLineFormatter extends Formatter { + private static final String SEP = System.getProperty("line.separator", "\n"); + + @Override + public synchronized String format(final LogRecord record) { + final boolean exception = record.getThrown() != null; + final StringBuilder sbuf = new StringBuilder(); + sbuf.append(record.getLevel().getLocalizedName()); + sbuf.append(" - "); + sbuf.append(this.formatMessage(record)); + sbuf.append(SEP); + if (exception) { + try { + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + record.getThrown().printStackTrace(pw); + pw.close(); + sbuf.append(sw.toString()); + } catch (final Exception ex) { + // no-op + } + } + return sbuf.toString(); + } + } } http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/JULLogger.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/JULLogger.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/JULLogger.java new file mode 100644 index 0000000..4e7e938 --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/JULLogger.java @@ -0,0 +1,179 @@ +/* + * 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.tomee.jul.formatter.log; + +import org.apache.juli.logging.Log; + +import java.util.logging.ConsoleHandler; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; + +// DirectJDKLog copy since it is now package scoped +public class JULLogger implements Log { + /** Alternate config reader and console format + */ + private static final String SIMPLE_FMT="java.util.logging.SimpleFormatter"; + private static final String SIMPLE_CFG="org.apache.juli.JdkLoggerConfig"; //doesn't exist + private static final String FORMATTER="org.apache.juli.formatter"; + + static { + if (System.getProperty("java.util.logging.config.class") == null && + System.getProperty("java.util.logging.config.file") == null) { + // default configuration - it sucks. Let's override at least the + // formatter for the console + try { + Class.forName(SIMPLE_CFG).newInstance(); + } catch(final Throwable t) { + // no-op + } + try { + final Formatter fmt=(Formatter)Class.forName(System.getProperty(FORMATTER, SIMPLE_FMT)).newInstance(); + // it is also possible that the user modified jre/lib/logging.properties - + // but that's really stupid in most cases + final Logger root=Logger.getLogger(""); + final Handler[] handlers = root.getHandlers(); + for( int i=0; i< handlers.length; i++ ) { + // I only care about console - that's what's used in default config anyway + if( handlers[i] instanceof ConsoleHandler) { + handlers[i].setFormatter(fmt); + } + } + } catch( Throwable t ) { + // no-op maybe it wasn't included - the ugly default will be used. + } + } + } + + private final Logger logger; + + public JULLogger(final String name ) { + logger= Logger.getLogger(name); + } + + @Override + public final boolean isErrorEnabled() { + return logger.isLoggable(Level.SEVERE); + } + + @Override + public final boolean isWarnEnabled() { + return logger.isLoggable(Level.WARNING); + } + + @Override + public final boolean isInfoEnabled() { + return logger.isLoggable(Level.INFO); + } + + @Override + public final boolean isDebugEnabled() { + return logger.isLoggable(Level.FINE); + } + + @Override + public final boolean isFatalEnabled() { + return logger.isLoggable(Level.SEVERE); + } + + @Override + public final boolean isTraceEnabled() { + return logger.isLoggable(Level.FINER); + } + + @Override + public final void debug(final Object message) { + log(Level.FINE, String.valueOf(message), null); + } + + @Override + public final void debug(final Object message, final Throwable t) { + log(Level.FINE, String.valueOf(message), t); + } + + @Override + public final void trace(final Object message) { + log(Level.FINER, String.valueOf(message), null); + } + + @Override + public final void trace(final Object message, final Throwable t) { + log(Level.FINER, String.valueOf(message), t); + } + + @Override + public final void info(final Object message) { + log(Level.INFO, String.valueOf(message), null); + } + + @Override + public final void info(final Object message, final Throwable t) { + log(Level.INFO, String.valueOf(message), t); + } + + @Override + public final void warn(final Object message) { + log(Level.WARNING, String.valueOf(message), null); + } + + @Override + public final void warn(final Object message, final Throwable t) { + log(Level.WARNING, String.valueOf(message), t); + } + + @Override + public final void error(final Object message) { + log(Level.SEVERE, String.valueOf(message), null); + } + + @Override + public final void error(final Object message, final Throwable t) { + log(Level.SEVERE, String.valueOf(message), t); + } + + @Override + public final void fatal(final Object message) { + log(Level.SEVERE, String.valueOf(message), null); + } + + @Override + public final void fatal(final Object message, final Throwable t) { + log(Level.SEVERE, String.valueOf(message), t); + } + + private void log(final Level level, final String msg, final Throwable ex) { + if (logger.isLoggable(level)) { + // Hack (?) to get the stack trace. + final Throwable dummyException=new Throwable(); + final StackTraceElement[] locations=dummyException.getStackTrace(); + // Caller will be the third element + String cname = "unknown"; + String method = "unknown"; + if (locations != null && locations.length > 3) { + final StackTraceElement caller = locations[3]; + cname = caller.getClassName(); + method = caller.getMethodName(); + } + if (ex==null) { + logger.logp(level, cname, method, msg); + } else { + logger.logp(level, cname, method, msg, ex); + } + } + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/LoggerFactory.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/LoggerFactory.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/LoggerFactory.java new file mode 100644 index 0000000..5e9f8ce --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/LoggerFactory.java @@ -0,0 +1,23 @@ +/* + * 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.tomee.jul.formatter.log; + +import org.apache.juli.logging.Log; + +public interface LoggerFactory { + Log newInstance(String name); +} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/NoopLogger.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/NoopLogger.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/NoopLogger.java new file mode 100644 index 0000000..a8c6a0c --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/NoopLogger.java @@ -0,0 +1,117 @@ +/* + * 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.tomee.jul.formatter.log; + +import org.apache.juli.logging.Log; + +public final class NoopLogger implements Log { + public static final NoopLogger INSTANCE = new NoopLogger(); + + private NoopLogger() { + // no-op + } + + @Override + public boolean isDebugEnabled() { + return false; + } + + @Override + public boolean isErrorEnabled() { + return false; + } + + @Override + public boolean isFatalEnabled() { + return false; + } + + @Override + public boolean isInfoEnabled() { + return false; + } + + @Override + public boolean isTraceEnabled() { + return false; + } + + @Override + public boolean isWarnEnabled() { + return false; + } + + @Override + public void trace(final Object message) { + // no-op + } + + @Override + public void trace(final Object message, final Throwable t) { + // no-op + } + + @Override + public void debug(final Object message) { + // no-op + } + + @Override + public void debug(final Object message, final Throwable t) { + // no-op + } + + @Override + public void info(final Object message) { + // no-op + } + + @Override + public void info(final Object message, final Throwable t) { + // no-op + } + + @Override + public void warn(final Object message) { + // no-op + } + + @Override + public void warn(final Object message, final Throwable t) { + // no-op + } + + @Override + public void error(final Object message) { + // no-op + } + + @Override + public void error(final Object message, final Throwable t) { + // no-op + } + + @Override + public void fatal(final Object message) { + // no-op + } + + @Override + public void fatal(final Object message, final Throwable t) { + // no-op + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/ReloadableLog.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/ReloadableLog.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/ReloadableLog.java new file mode 100644 index 0000000..9ca1061 --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/ReloadableLog.java @@ -0,0 +1,111 @@ +/* + * 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.tomee.jul.formatter.log; + +import org.apache.juli.logging.Log; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicReference; + +public final class ReloadableLog { + + public static final Class<?>[] INTERFACES = new Class<?>[]{Log.class}; + + private ReloadableLog() { + // no-op + } + + public static Log newLog(final String name, final String factory) { + return Log.class.cast(Proxy.newProxyInstance( + ReloadableLog.class.getClassLoader(), INTERFACES, new ReloadableLogHandler(factory, name))); + } + + private static final class ReloadableLogHandler implements InvocationHandler { + private static final String LOG4J_IMPL = "org.apache.tomee.loader.log.Log4jLog"; + private static final String LOG4J2_IMPL = "org.apache.tomee.loader.log.Log4j2Log"; + private static final String SLF4J_IMPL = "org.apache.tomee.loader.log.Slf4jLog"; + private static final String MAVEN_IMPL = "org.apache.openejb.maven.util.TomEEMavenLog"; + + private volatile String factory; + private final String name; + private final AtomicReference<Log> delegate = new AtomicReference<Log>(); + private volatile boolean done = false; + + public ReloadableLogHandler(final String factory, final String name) { + this.factory = factory; + this.name = name; + initDelegate(); + } + + private Log initDelegate() { + if (done) { + return delegate.get(); + } + + try { + if (factory == null) { + final String f = TomEELog.getLoggerClazz(); + if (f != null) { + factory = f; + } + + final Log log = delegate.get(); + if (factory == null && log != null) { + return log; + } + } + if ("org.apache.openejb.util.Log4jLogStreamFactory".equals(factory)) { + delegate.set(newInstance(LOG4J_IMPL)); + } else if ("org.apache.openejb.util.Log4j2LogStreamFactory".equals(factory)) { + delegate.set(newInstance(LOG4J2_IMPL)); + } else if ("org.apache.openejb.util.Slf4jLogStreamFactory".equals(factory)) { + delegate.set(newInstance(SLF4J_IMPL)); + } else if ("org.apache.openejb.maven.util.MavenLogStreamFactory".equals(factory)) { + delegate.set(newInstance(MAVEN_IMPL)); + } else { + delegate.set(new JULLogger(name)); + } + done = true; + } catch (final Throwable the) { + if (delegate.get() == null) { + delegate.set(new JULLogger(name)); + } + } + return delegate.get(); + } + + private Log newInstance(final String impl) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, ClassNotFoundException { + return Log.class.cast(Thread.currentThread() + .getContextClassLoader() + .loadClass(impl) + .getConstructor(String.class) + .newInstance(name)); + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + try { + return method.invoke(initDelegate(), args); + } catch (final InvocationTargetException ite) { + throw ite.getCause(); + } + } + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/TomEELog.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/TomEELog.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/TomEELog.java new file mode 100644 index 0000000..422317c --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/formatter/log/TomEELog.java @@ -0,0 +1,164 @@ +/* + * 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.tomee.jul.formatter.log; + +import org.apache.juli.logging.Log; + +import java.lang.reflect.Method; + +public class TomEELog implements Log { + private static volatile boolean initialized; + private static volatile String loggerClazz; + private static volatile boolean defaultLogger; + + private static synchronized void initialize() { + if (initialized) { + return; + } + + if (!Boolean.getBoolean("tomee.skip-tomcat-log")) { + final Thread thread = Thread.currentThread(); + try { + final ClassLoader tccl = thread.getContextClassLoader(); + final Class<?> systemInstance = tccl.loadClass("org.apache.openejb.loader.SystemInstance"); + if (!Boolean.class.cast(systemInstance.getMethod("isInitialized").invoke(null))) { + return; + } + + final Class<?> logger = tccl.loadClass("org.apache.openejb.util.Logger"); + final Method m = logger.getDeclaredMethod("delegateClass"); + loggerClazz = (String) m.invoke(null); + if ("org.apache.openejb.util.Log4j2LogStreamFactory".equals(loggerClazz) + || "org.apache.openejb.util.Log4jLogStreamFactory".equals(loggerClazz) + || "org.apache.openejb.util.Slf4jLogStreamFactory".equals(loggerClazz) + || "org.apache.openejb.maven.util.MavenLogStreamFactory".equals(loggerClazz)) { + defaultLogger = false; + } else { + defaultLogger = false; + } + initialized = true; + } catch (final Throwable th) { + // no-op + } + } + } + + public static String getLoggerClazz() { + return loggerClazz; + } + + private final Log delegate; + + public TomEELog() { // for ServiceLoader + delegate = null; + } + + public TomEELog(final String name) { + initialize(); + this.delegate = defaultLogger ? new JULLogger(name) : ReloadableLog.newLog(name, loggerClazz); + } + + @Override + public boolean isDebugEnabled() { + return delegate.isDebugEnabled(); + } + + @Override + public boolean isErrorEnabled() { + return delegate.isErrorEnabled(); + } + + @Override + public boolean isFatalEnabled() { + return delegate.isFatalEnabled(); + } + + @Override + public boolean isInfoEnabled() { + return delegate.isInfoEnabled(); + } + + @Override + public boolean isTraceEnabled() { + return delegate.isTraceEnabled(); + } + + @Override + public boolean isWarnEnabled() { + return delegate.isWarnEnabled(); + } + + @Override + public void trace(final Object message) { + delegate.trace(message); + } + + @Override + public void trace(final Object message, final Throwable t) { + delegate.trace(message, t); + } + + @Override + public void debug(final Object message) { + delegate.debug(message); + } + + @Override + public void debug(final Object message, final Throwable t) { + delegate.debug(message, t); + } + + @Override + public void info(final Object message) { + delegate.info(message); + } + + @Override + public void info(final Object message, final Throwable t) { + delegate.info(message, t); + } + + @Override + public void warn(final Object message) { + delegate.warn(message); + } + + @Override + public void warn(final Object message, final Throwable t) { + delegate.warn(message, t); + } + + @Override + public void error(final Object message) { + delegate.error(message); + } + + @Override + public void error(final Object message, final Throwable t) { + delegate.error(message, t); + } + + @Override + public void fatal(final Object message) { + delegate.fatal(message); + } + + @Override + public void fatal(final Object message, final Throwable t) { + delegate.fatal(message, t); + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/BackgroundTaskRunner.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/BackgroundTaskRunner.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/BackgroundTaskRunner.java new file mode 100644 index 0000000..5b88b5f --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/BackgroundTaskRunner.java @@ -0,0 +1,91 @@ +/* + * 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.tomee.jul.handler.rotating; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.LogManager; + +// Note: don't touch this class while not needed to avoid to trigger the executor service init +// mainly there to avoid all handlers to have their own threads +class BackgroundTaskRunner { + private static final ExecutorService EXECUTOR_SERVICE; + private static final boolean SYNCHRONOUS; + + static { + final LogManager logManager = LogManager.getLogManager(); + SYNCHRONOUS = Boolean.parseBoolean(getProperty(logManager, BackgroundTaskRunner.class.getName() + ".synchronous")); + if (SYNCHRONOUS) { + EXECUTOR_SERVICE = null; + } else { + + final String threadCount = getProperty(logManager, BackgroundTaskRunner.class.getName() + ".threads"); + final String shutdownTimeoutStr = getProperty(logManager, BackgroundTaskRunner.class.getName() + ".shutdownTimeout"); + final Duration shutdownTimeout = new Duration(shutdownTimeoutStr == null ? "30 seconds" : shutdownTimeoutStr); + EXECUTOR_SERVICE = Executors.newFixedThreadPool(Integer.parseInt(threadCount == null ? "2" : threadCount), new ThreadFactory() { + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix = "com.tomitribe.logging.jul.handler.BackgroundTaskThread-"; + + { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); + } + + @Override + public Thread newThread(final Runnable r) { + final Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); + if (!t.isDaemon()) { + t.setDaemon(true); + } + if (t.getPriority() != Thread.NORM_PRIORITY) { + t.setPriority(Thread.NORM_PRIORITY); + } + return t; + } + }); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + EXECUTOR_SERVICE.shutdown(); + try { + EXECUTOR_SERVICE.awaitTermination(shutdownTimeout.asMillis(), TimeUnit.MILLISECONDS); + } catch (final InterruptedException e) { + Thread.interrupted(); + } + } + }); + } + } + + private static String getProperty(final LogManager logManager, final String key) { + final String val = logManager.getProperty(key); + return val != null ? val : System.getProperty(key); + } + + static void push(final Runnable runnable) { + if (SYNCHRONOUS) { + runnable.run(); + } else { + EXECUTOR_SERVICE.submit(runnable); + } + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Duration.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Duration.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Duration.java new file mode 100644 index 0000000..005f43a --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Duration.java @@ -0,0 +1,178 @@ +/* + * 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.tomee.jul.handler.rotating; + +import java.util.concurrent.TimeUnit; + +class Duration { + private long time; + private TimeUnit unit = TimeUnit.MILLISECONDS; + + private Duration() { + // no-op + } + + private Duration(final long time, final TimeUnit unit) { + this.time = time; + this.unit = unit; + } + + Duration(final String string) { + this(string, null); + } + + private Duration(final String string, final TimeUnit defaultUnit) { + final String[] strings = string.split(",| and "); + + Duration total = new Duration(); + + for (final String value : strings) { + final Duration part = new Duration(); + final String s = value.trim(); + + final StringBuilder t = new StringBuilder(); + final StringBuilder u = new StringBuilder(); + + int i = 0; + + // get the number + for (; i < s.length(); i++) { + final char c = s.charAt(i); + if (Character.isDigit(c) || i == 0 && c == '-') { + t.append(c); + } else { + break; + } + } + + if (t.length() == 0) { + invalidFormat(s); + } + + // skip whitespace + for (; i < s.length(); i++) { + final char c = s.charAt(i); + if (!Character.isWhitespace(c)) { + break; + } + } + + // get time unit text part + for (; i < s.length(); i++) { + final char c = s.charAt(i); + if (Character.isLetter(c)) { + u.append(c); + } else { + invalidFormat(s); + } + } + + part.time = Long.parseLong(t.toString()); + + part.unit = parseUnit(u.toString()); + + if (part.unit == null) { + part.unit = defaultUnit; + } + + total = total.add(part); + } + + this.time = total.time; + this.unit = total.unit; + } + + public long asMillis() { + return unit.toMillis(time); + } + + private static class Normalize { + private long a; + private long b; + private TimeUnit base; + + private Normalize(final Duration a, final Duration b) { + this.base = lowest(a, b); + this.a = a.unit == null ? a.time : base.convert(a.time, a.unit); + this.b = b.unit == null ? b.time : base.convert(b.time, b.unit); + } + + private static TimeUnit lowest(final Duration a, final Duration b) { + if (a.unit == null) return b.unit; + if (b.unit == null) return a.unit; + if (a.time == 0) return b.unit; + if (b.time == 0) return a.unit; + return TimeUnit.values()[Math.min(a.unit.ordinal(), b.unit.ordinal())]; + } + } + + public Duration add(final Duration that) { + final Normalize n = new Normalize(this, that); + return new Duration(n.a + n.b, n.base); + } + + private static void invalidFormat(final String text) { + throw new IllegalArgumentException("Illegal duration format: '" + text + + "'. Valid examples are '10s' or '10 seconds'."); + } + + private static TimeUnit parseUnit(final String u) { + if (u.length() == 0) { + return null; + } + + if ("NANOSECONDS".equalsIgnoreCase(u)) return TimeUnit.NANOSECONDS; + if ("NANOSECOND".equalsIgnoreCase(u)) return TimeUnit.NANOSECONDS; + if ("NANOS".equalsIgnoreCase(u)) return TimeUnit.NANOSECONDS; + if ("NANO".equalsIgnoreCase(u)) return TimeUnit.NANOSECONDS; + if ("NS".equalsIgnoreCase(u)) return TimeUnit.NANOSECONDS; + + if ("MICROSECONDS".equalsIgnoreCase(u)) return TimeUnit.MICROSECONDS; + if ("MICROSECOND".equalsIgnoreCase(u)) return TimeUnit.MICROSECONDS; + if ("MICROS".equalsIgnoreCase(u)) return TimeUnit.MICROSECONDS; + if ("MICRO".equalsIgnoreCase(u)) return TimeUnit.MICROSECONDS; + + if ("MILLISECONDS".equalsIgnoreCase(u)) return TimeUnit.MILLISECONDS; + if ("MILLISECOND".equalsIgnoreCase(u)) return TimeUnit.MILLISECONDS; + if ("MILLIS".equalsIgnoreCase(u)) return TimeUnit.MILLISECONDS; + if ("MILLI".equalsIgnoreCase(u)) return TimeUnit.MILLISECONDS; + if ("MS".equalsIgnoreCase(u)) return TimeUnit.MILLISECONDS; + + if ("SECONDS".equalsIgnoreCase(u)) return TimeUnit.SECONDS; + if ("SECOND".equalsIgnoreCase(u)) return TimeUnit.SECONDS; + if ("SEC".equalsIgnoreCase(u)) return TimeUnit.SECONDS; + if ("S".equalsIgnoreCase(u)) return TimeUnit.SECONDS; + + if ("MINUTES".equalsIgnoreCase(u)) return TimeUnit.MINUTES; + if ("MINUTE".equalsIgnoreCase(u)) return TimeUnit.MINUTES; + if ("MIN".equalsIgnoreCase(u)) return TimeUnit.MINUTES; + if ("M".equalsIgnoreCase(u)) return TimeUnit.MINUTES; + + if ("HOURS".equalsIgnoreCase(u)) return TimeUnit.HOURS; + if ("HOUR".equalsIgnoreCase(u)) return TimeUnit.HOURS; + if ("HRS".equalsIgnoreCase(u)) return TimeUnit.HOURS; + if ("HR".equalsIgnoreCase(u)) return TimeUnit.HOURS; + if ("H".equalsIgnoreCase(u)) return TimeUnit.HOURS; + + if ("DAYS".equalsIgnoreCase(u)) return TimeUnit.DAYS; + if ("DAY".equalsIgnoreCase(u)) return TimeUnit.DAYS; + if ("D".equalsIgnoreCase(u)) return TimeUnit.DAYS; + + throw new IllegalArgumentException("Unknown time unit '" + u + "'"); + } +} + http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/LocalFileHandler.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/LocalFileHandler.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/LocalFileHandler.java new file mode 100644 index 0000000..e4217ad --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/LocalFileHandler.java @@ -0,0 +1,593 @@ +/* + * 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.tomee.jul.handler.rotating; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; +import java.sql.Timestamp; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.ErrorManager; +import java.util.logging.Filter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.LogRecord; +import java.util.logging.SimpleFormatter; +import java.util.regex.Pattern; +import java.util.zip.Deflater; +import java.util.zip.GZIPOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * NOTE: for simplicity the prefix `org.apache.tomee.jul.handler.rotating.LocalFileHandler.` has been removed of name columns. + * + * |=== + * | Name | Default Value | Description + * | filenamePattern | ${catalina.base}/logs/logs.%s.%03d.log | where log files are created, it uses String.format() and gives you the date and file number - in this order. + * | limit | 10 Megabytes | limit size indicating the file should be rotated + * | dateCheckInterval | 5 seconds | how often the date should be computed to rotate the file (don't do it each time for performances reason, means you can get few records of next day in a file name with current day) + * | bufferSize | -1 bytes | if positive the in memory buffer used to store data before flushing them to the disk + * | encoding | - | file encoding + * | level | ALL | level this handler accepts + * | filter | - | filter used to check if the message should be logged + * | formatter | java.util.logging.SimpleFormatter | formatter used to format messages + * | archiveDirectory | ${catalina.base}/logs/archives/ | where compressed logs are put. + * | archiveFormat | gzip | zip or gzip. + * | archiveOlderThan | -1 days | how many days files are kept before being compressed + * | purgeOlderThan | -1 days | how many days files are kept before being deleted, note: it applies on archives and not log files so 2 days of archiving and 3 days of purge makes it deleted after 5 days. + * | compressionLevel | -1 | In case of zip archiving the zip compression level (-1 for off or 0-9). + * |=== + * + * NOTE: archiving and purging are done only when a file is rotated, it means it can be ignored during days if there is no logging activity. + * + * NOTE: archiving and purging is done in a background thread pool, you can configure the number of threads in thanks to + * `org.apache.tomee.jul.handler.rotating.BackgroundTaskRunner.threads` property in `conf/logging.properties`. + * Default is 2 which should be fine for most applications. + */ +/* + Open point/enhancements: + - date pattern/filename pattern instead of hardcoded String.format? + - write another async version? ensure it flushed well, use disruptor? -> bench seems to show it is useless + */ +public class LocalFileHandler extends Handler { + private static final int BUFFER_SIZE = 8102; + + private long limit = 0; + private int bufferSize = -1; + private Pattern filenameRegex; + private Pattern archiveFilenameRegex; + private String filenamePattern = "${catalina.base}/logs/logs.%s.%03d.log"; + private String archiveFormat = "gzip"; + private long dateCheckInterval = TimeUnit.SECONDS.toMillis(5); + private long archiveExpiryDuration; + private int compressionLevel; + private long purgeExpiryDuration; + private File archiveDir; + + private volatile int currentIndex; + private volatile long lastTimestamp; + private volatile String date; + private volatile PrintWriter writer; + private volatile int written; + private final ReadWriteLock writerLock = new ReentrantReadWriteLock(); + private final Lock backgroundTaskLock = new ReentrantLock(); + private volatile boolean closed; + + public LocalFileHandler() { + configure(); + } + + private void configure() { + date = currentDate(); + + final String className = getClass().getName(); //allow classes to override + + final ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + dateCheckInterval = new Duration(getProperty(className + ".dateCheckInterval", String.valueOf(dateCheckInterval))).asMillis(); + filenamePattern = replace(getProperty(className + ".filenamePattern", filenamePattern)); + limit = new Size(getProperty(className + ".limit", String.valueOf("10 Mega"))).asBytes(); + + final int lastSep = Math.max(filenamePattern.lastIndexOf('/'), filenamePattern.lastIndexOf('\\')); + String fileNameReg = lastSep >= 0 ? filenamePattern.substring(lastSep + 1) : filenamePattern; + fileNameReg = fileNameReg.replace("%s", "\\d{4}\\-\\d{2}\\-\\d{2}"); // date. + { // file rotation index + final int indexIdxStart = fileNameReg.indexOf('%'); + if (indexIdxStart >= 0) { + final int indexIdxEnd = fileNameReg.indexOf('d', indexIdxStart); + if (indexIdxEnd >= 0) { + fileNameReg = fileNameReg.substring(0, indexIdxStart) + "\\d*" + fileNameReg.substring(indexIdxEnd + 1, fileNameReg.length()); + } + } + } + filenameRegex = Pattern.compile(fileNameReg); + + compressionLevel = Integer.parseInt(getProperty(className + ".compressionLevel", String.valueOf(Deflater.DEFAULT_COMPRESSION))); + archiveExpiryDuration = new Duration(getProperty(className + ".archiveOlderThan", String.valueOf("-1 days"))).asMillis(); + archiveDir = new File(replace(getProperty(className + ".archiveDirectory", "${catalina.base}/logs/archives/"))); + archiveFormat = replace(getProperty(className + ".archiveFormat", archiveFormat)); + archiveFilenameRegex = Pattern.compile(fileNameReg + "\\." + archiveFormat); + + purgeExpiryDuration = new Duration(getProperty(className + ".purgeOlderThan", String.valueOf("-1 days"))).asMillis(); + + try { + bufferSize = (int) new Size(getProperty(className + ".bufferSize", "-1 b")).asBytes(); + } catch (final NumberFormatException ignore) { + // no-op + } + + final String encoding = getProperty(className + ".encoding", null); + if (encoding != null && encoding.length() > 0) { + try { + setEncoding(encoding); + } catch (final UnsupportedEncodingException ex) { + // no-op + } + } + + setLevel(Level.parse(getProperty(className + ".level", "" + Level.ALL))); + + final String filterName = getProperty(className + ".filter", null); + if (filterName != null) { + try { + setFilter(Filter.class.cast(cl.loadClass(filterName).newInstance())); + } catch (final Exception e) { + // Ignore + } + } + + final String formatterName = getProperty(className + ".formatter", null); + if (formatterName != null) { + try { + setFormatter(Formatter.class.cast(cl.loadClass(formatterName).newInstance())); + } catch (final Exception e) { + setFormatter(new SimpleFormatter()); + } + } else { + setFormatter(new SimpleFormatter()); + } + + setErrorManager(new ErrorManager()); + + lastTimestamp = System.currentTimeMillis(); + } + + protected String currentDate() { + return new Timestamp(System.currentTimeMillis()).toString().substring(0, 10); + } + + @Override + public void publish(final LogRecord record) { + if (!isLoggable(record)) { + return; + } + + final long now = System.currentTimeMillis(); + final String tsDate; + // just do it once / sec if we have a lot of log, can make some log appearing in the wrong file but better than doing it each time + if (now - lastTimestamp > dateCheckInterval) { // using as much as possible volatile to avoid to lock too much + lastTimestamp = now; + tsDate = currentDate(); + } else { + tsDate = null; + } + + try { + writerLock.readLock().lock(); + rotateIfNeeded(tsDate); + + String result; + try { + result = getFormatter().format(record); + } catch (final Exception e) { + reportError(null, e, ErrorManager.FORMAT_FAILURE); + return; + } + + try { + if (writer != null) { + writer.write(result); + if (bufferSize < 0) { + writer.flush(); + } + } else { + reportError(getClass().getSimpleName() + " is closed or not yet initialized, unable to log [" + result + "]", null, ErrorManager.WRITE_FAILURE); + } + } catch (final Exception e) { + reportError(null, e, ErrorManager.WRITE_FAILURE); + } + } finally { + writerLock.readLock().unlock(); + } + } + + private void rotateIfNeeded(final String currentDate) { + if (!closed && writer == null) { + try { + writerLock.readLock().unlock(); + writerLock.writeLock().lock(); + + if (!closed && writer == null) { + openWriter(); + } + } finally { + writerLock.writeLock().unlock(); + writerLock.readLock().lock(); + } + } else if (shouldRotate(currentDate)) { + try { + writerLock.readLock().unlock(); + writerLock.writeLock().lock(); + + if (shouldRotate(currentDate)) { + close(); + if (currentDate != null && !date.equals(currentDate)) { + currentIndex = 0; + date = currentDate; + } + openWriter(); + } + } finally { + writerLock.writeLock().unlock(); + writerLock.readLock().lock(); + } + } + } + + private boolean shouldRotate(final String currentDate) { // new day, new file or limit exceeded + return (currentDate != null && !date.equals(currentDate)) || (limit > 0 && written >= limit); + } + + @Override + public void close() { + closed = true; + + writerLock.writeLock().lock(); + try { + if (writer == null) { + return; + } + writer.write(getFormatter().getTail(this)); + writer.flush(); + writer.close(); + writer = null; + } catch (final Exception e) { + reportError(null, e, ErrorManager.CLOSE_FAILURE); + } finally { + writerLock.writeLock().unlock(); + } + + // wait for bg tasks if running + backgroundTaskLock.lock(); + backgroundTaskLock.unlock(); + } + + @Override + public void flush() { + writerLock.readLock().lock(); + try { + writer.flush(); + } catch (final Exception e) { + reportError(null, e, ErrorManager.FLUSH_FAILURE); + } finally { + writerLock.readLock().unlock(); + } + } + + protected void openWriter() { + final long beforeRotation = System.currentTimeMillis(); + + writerLock.writeLock().lock(); + FileOutputStream fos = null; + OutputStream os = null; + try { + File pathname; + do { + pathname = new File(formatFilename(filenamePattern, date, currentIndex)); + final File parent = pathname.getParentFile(); + if (!parent.isDirectory() && !parent.mkdirs()) { + reportError("Unable to create [" + parent + "]", null, ErrorManager.OPEN_FAILURE); + writer = null; + return; + } + currentIndex++; + } while (pathname.isFile()); // loop to ensure we don't overwrite existing files + + final String encoding = getEncoding(); + fos = new FileOutputStream(pathname, true); + os = new CountingStream(bufferSize > 0 ? new BufferedOutputStream(fos, bufferSize) : fos); + writer = new PrintWriter((encoding != null) ? new OutputStreamWriter(os, encoding) : new OutputStreamWriter(os), false); + writer.write(getFormatter().getHead(this)); + } catch (final Exception e) { + reportError(null, e, ErrorManager.OPEN_FAILURE); + writer = null; + if (fos != null) { + try { + fos.close(); + } catch (final IOException e1) { + // no-op + } + } + if (os != null) { + try { + os.close(); + } catch (final IOException e1) { + // no-op + } + } + } finally { + writerLock.writeLock().unlock(); + } + + BackgroundTaskRunner.push(new Runnable() { + @Override + public void run() { + backgroundTaskLock.lock(); + try { + evict(beforeRotation); + } catch (final Exception e) { + reportError("Can't do the log eviction", e, ErrorManager.GENERIC_FAILURE); + } finally { + backgroundTaskLock.unlock(); + } + } + }); + } + + private void evict(final long now) { + if (purgeExpiryDuration > 0) { // purging archives + final File[] archives = archiveDir.listFiles(new FilenameFilter() { + @Override + public boolean accept(final File dir, final String name) { + return archiveFilenameRegex.matcher(name).matches(); + } + }); + + if (archives != null) { + for (final File archive : archives) { + try { + final BasicFileAttributes attr = Files.readAttributes(archive.toPath(), BasicFileAttributes.class); + if (now - attr.creationTime().toMillis() > purgeExpiryDuration) { + if (!Files.deleteIfExists(archive.toPath())) { + // dont try to delete on exit cause we will find it again + reportError("Can't delete " + archive.getAbsolutePath() + ".", null, ErrorManager.GENERIC_FAILURE); + } + } + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + } + } + if (archiveExpiryDuration > 0) { // archiving log files + final File[] logs = new File(formatFilename(filenamePattern, "0000-00-00", 0)).getParentFile() + .listFiles(new FilenameFilter() { + @Override + public boolean accept(final File dir, final String name) { + return filenameRegex.matcher(name).matches(); + } + }); + + if (logs != null) { + for (final File file : logs) { + try { + final BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class); + if (attr.creationTime().toMillis() < now && now - attr.lastModifiedTime().toMillis() > archiveExpiryDuration) { + createArchive(file); + } + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + } + } + } + + private String formatFilename(final String pattern, final String date, final int index) { + return String.format(pattern, date, index); + } + + private void createArchive(final File source) { + final File target = new File(archiveDir, source.getName() + "." + archiveFormat); + if (target.isFile()) { + return; + } + + final File parentFile = target.getParentFile(); + if (!parentFile.isDirectory() && !parentFile.mkdirs()) { + throw new IllegalStateException("Can't create " + parentFile.getAbsolutePath()); + } + + if (archiveFormat.equalsIgnoreCase("gzip")) { + OutputStream outputStream = null; + try { + outputStream = new BufferedOutputStream(new GZIPOutputStream(new FileOutputStream(target))); + final byte[] buffer = new byte[BUFFER_SIZE]; + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(source); + copyStream(inputStream, outputStream, buffer); + } catch (final IOException e) { + throw new IllegalStateException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (final IOException ioe) { + // no-op + } + } + } + } catch (final IOException e) { + throw new IllegalStateException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (final IOException ioe) { + // no-op + } + } + } + } else { // consider file defines a zip whatever extension it is + ZipOutputStream outputStream = null; + try { + outputStream = new ZipOutputStream(new FileOutputStream(target)); + outputStream.setLevel(compressionLevel); + + final byte[] buffer = new byte[BUFFER_SIZE]; + FileInputStream inputStream = null; + try { + inputStream = new FileInputStream(source); + final ZipEntry zipEntry = new ZipEntry(source.getName()); + outputStream.putNextEntry(zipEntry); + copyStream(inputStream, outputStream, buffer); + } catch (final IOException e) { + throw new IllegalStateException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (final IOException ioe) { + // no-op + } + } + } + } catch (final IOException e) { + throw new IllegalStateException(e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (final IOException ioe) { + // no-op + } + } + } + } + try { + if (!Files.deleteIfExists(source.toPath())) { + reportError("Can't delete " + source.getAbsolutePath() + ".", null, ErrorManager.GENERIC_FAILURE); + } + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + + private static void copyStream(final InputStream inputStream, final OutputStream outputStream, final byte[] buffer) throws IOException { + int n; + while ((n = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, n); + } + } + + protected String getProperty(final String name, final String defaultValue) { + String value = LogManager.getLogManager().getProperty(name); + if (value == null) { + value = defaultValue; + } else { + value = value.trim(); + } + return value; + } + + protected static String replace(final String str) { // [lang3] would be good but no dep for these classes is better + String result = str; + int start = str.indexOf("${"); + if (start >= 0) { + final StringBuilder builder = new StringBuilder(); + int end = -1; + while (start >= 0) { + builder.append(str, end + 1, start); + end = str.indexOf('}', start + 2); + if (end < 0) { + end = start - 1; + break; + } + + final String propName = str.substring(start + 2, end); + String replacement = !propName.isEmpty() ? System.getProperty(propName) : null; + if (replacement == null) { + replacement = System.getenv(propName); + } + if (replacement != null) { + builder.append(replacement); + } else { + builder.append(str, start, end + 1); + } + start = str.indexOf("${", end + 1); + } + builder.append(str, end + 1, str.length()); + result = builder.toString(); + } + return result; + } + + private class CountingStream extends OutputStream { + private final OutputStream out; + + private CountingStream(final OutputStream out) { + this.out = out; + written = 0; + } + + @Override + public void write(final int b) throws IOException { + out.write(b); + written++; + } + + @Override + public void write(final byte buff[]) throws IOException { + out.write(buff); + written += buff.length; + } + + @Override + public void write(final byte buff[], final int off, final int len) throws IOException { + out.write(buff, off, len); + written += len; + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void close() throws IOException { + out.close(); + } + } +} http://git-wip-us.apache.org/repos/asf/tomee/blob/da4df975/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Size.java ---------------------------------------------------------------------- diff --git a/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Size.java b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Size.java new file mode 100644 index 0000000..be1b476 --- /dev/null +++ b/tomee/tomee-juli/src/main/java/org/apache/tomee/jul/handler/rotating/Size.java @@ -0,0 +1,343 @@ +/* + * 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.tomee.jul.handler.rotating; + +import static java.lang.Long.MAX_VALUE; + +class Size { + private long size; + private SizeUnit unit; + + private Size() { + // no-op + } + + private Size(final long size, final SizeUnit unit) { + this.size = size; + this.unit = unit; + } + + Size(final String string) { + this(string, null); + } + + private Size(final String string, final SizeUnit defaultUnit) { + final String[] strings = string.split(",| and "); + + Size total = new Size(); + for (String s : strings) { + final Size part = new Size(); + s = s.trim(); + + final StringBuilder t = new StringBuilder(); + final StringBuilder u = new StringBuilder(); + + int i = 0; + + // get the number + for (; i < s.length(); i++) { + final char c = s.charAt(i); + if (Character.isDigit(c) || i == 0 && c == '-' || i > 0 && c == '.') { + t.append(c); + } else { + break; + } + } + + if (t.length() == 0) { + invalidFormat(s); + } + + // skip whitespace + for (; i < s.length(); i++) { + final char c = s.charAt(i); + if (!Character.isWhitespace(c)) { + break; + } + } + + // get time unit text part + for (; i < s.length(); i++) { + final char c = s.charAt(i); + if (Character.isLetter(c)) { + u.append(c); + } else { + invalidFormat(s); + } + } + + + part.unit = parseUnit(u.toString()); + + if (part.unit == null) { + part.unit = defaultUnit; + } + + final String size = t.toString(); + if (size.contains(".")) { + if (part.unit == null) { + throw new IllegalArgumentException("unit must be specified with floating point numbers"); + } + final double d = Double.parseDouble(size); + final long bytes = part.unit.toBytes(1); + part.size = (long) (bytes * d); + part.unit = SizeUnit.BYTES; + } else { + part.size = Integer.parseInt(size); + } + + total = total.add(part); + } + + this.size = total.size; + this.unit = total.unit; + } + + public long asBytes() { + return unit.toBytes(size); + } + + private static class Normalize { + private long a; + private long b; + private SizeUnit base; + + private Normalize(final Size a, final Size b) { + this.base = lowest(a, b); + this.a = a.unit == null ? a.size : base.convert(a.size, a.unit); + this.b = b.unit == null ? b.size : base.convert(b.size, b.unit); + } + + private static SizeUnit lowest(final Size a, final Size b) { + if (a.unit == null) return b.unit; + if (b.unit == null) return a.unit; + if (a.size == 0) return b.unit; + if (b.size == 0) return a.unit; + return SizeUnit.values()[Math.min(a.unit.ordinal(), b.unit.ordinal())]; + } + } + + public Size add(final Size that) { + final Normalize n = new Normalize(this, that); + return new Size(n.a + n.b, n.base); + } + + private static void invalidFormat(final String text) { + throw new IllegalArgumentException("Illegal size format: '" + text + "'. Valid examples are '10kb' or '10 kilobytes'."); + } + + private static SizeUnit parseUnit(final String u) { + if (u.length() == 0) return null; + + if ("BYTES".equalsIgnoreCase(u)) return SizeUnit.BYTES; + if ("BYTE".equalsIgnoreCase(u)) return SizeUnit.BYTES; + if ("B".equalsIgnoreCase(u)) return SizeUnit.BYTES; + + if ("KILOBYTES".equalsIgnoreCase(u)) return SizeUnit.KILOBYTES; + if ("KILOBYTE".equalsIgnoreCase(u)) return SizeUnit.KILOBYTES; + if ("KILO".equalsIgnoreCase(u)) return SizeUnit.KILOBYTES; + if ("KB".equalsIgnoreCase(u)) return SizeUnit.KILOBYTES; + if ("K".equalsIgnoreCase(u)) return SizeUnit.KILOBYTES; + + if ("MEGABYTES".equalsIgnoreCase(u)) return SizeUnit.MEGABYTES; + if ("MEGABYTE".equalsIgnoreCase(u)) return SizeUnit.MEGABYTES; + if ("MEGA".equalsIgnoreCase(u)) return SizeUnit.MEGABYTES; + if ("MB".equalsIgnoreCase(u)) return SizeUnit.MEGABYTES; + if ("M".equalsIgnoreCase(u)) return SizeUnit.MEGABYTES; + + if ("GIGABYTES".equalsIgnoreCase(u)) return SizeUnit.GIGABYTES; + if ("GIGABYTE".equalsIgnoreCase(u)) return SizeUnit.GIGABYTES; + if ("GIGA".equalsIgnoreCase(u)) return SizeUnit.GIGABYTES; + if ("GB".equalsIgnoreCase(u)) return SizeUnit.GIGABYTES; + if ("G".equalsIgnoreCase(u)) return SizeUnit.GIGABYTES; + + throw new IllegalArgumentException("Unknown size unit '" + u + "'"); + } + + private enum SizeUnit { + BYTES { + public long toBytes(final long s) { + return s; + } + + public long toKilobytes(final long s) { + return s / (B1 / B0); + } + + public long toMegabytes(final long s) { + return s / (B2 / B0); + } + + public long toGigabytes(final long s) { + return s / (B3 / B0); + } + + public long toTerabytes(final long s) { + return s / (B4 / B0); + } + + public long convert(final long s, final SizeUnit u) { + return u.toBytes(s); + } + }, + + KILOBYTES { + public long toBytes(final long s) { + return x(s, B1 / B0, MAX_VALUE / (B1 / B0)); + } + + public long toKilobytes(final long s) { + return s; + } + + public long toMegabytes(final long s) { + return s / (B2 / B1); + } + + public long toGigabytes(final long s) { + return s / (B3 / B1); + } + + public long toTerabytes(final long s) { + return s / (B4 / B1); + } + + public long convert(final long s, final SizeUnit u) { + return u.toKilobytes(s); + } + }, + + MEGABYTES { + public long toBytes(final long s) { + return x(s, B2 / B0, MAX_VALUE / (B2 / B0)); + } + + public long toKilobytes(final long s) { + return x(s, B2 / B1, MAX_VALUE / (B2 / B1)); + } + + public long toMegabytes(final long s) { + return s; + } + + public long toGigabytes(final long s) { + return s / (B3 / B2); + } + + public long toTerabytes(final long s) { + return s / (B4 / B2); + } + + public long convert(final long s, final SizeUnit u) { + return u.toMegabytes(s); + } + }, + + GIGABYTES { + public long toBytes(final long s) { + return x(s, B3 / B0, MAX_VALUE / (B3 / B0)); + } + + public long toKilobytes(final long s) { + return x(s, B3 / B1, MAX_VALUE / (B3 / B1)); + } + + public long toMegabytes(final long s) { + return x(s, B3 / B2, MAX_VALUE / (B3 / B2)); + } + + public long toGigabytes(final long s) { + return s; + } + + public long toTerabytes(final long s) { + return s / (B4 / B3); + } + + public long convert(final long s, final SizeUnit u) { + return u.toGigabytes(s); + } + }, + + TERABYTES { + public long toBytes(final long s) { + return x(s, B4 / B0, MAX_VALUE / (B4 / B0)); + } + + public long toKilobytes(final long s) { + return x(s, B4 / B1, MAX_VALUE / (B4 / B1)); + } + + public long toMegabytes(final long s) { + return x(s, B4 / B2, MAX_VALUE / (B4 / B2)); + } + + public long toGigabytes(final long s) { + return x(s, B4 / B3, MAX_VALUE / (B4 / B3)); + } + + public long toTerabytes(final long s) { + return s; + } + + public long convert(final long s, final SizeUnit u) { + return u.toTerabytes(s); + } + }; + + static final long B0 = 1L; + static final long B1 = B0 * 1024L; + static final long B2 = B1 * 1024L; + static final long B3 = B2 * 1024L; + static final long B4 = B3 * 1024L; + + + static long x(final long d, final long m, final long over) { + if (d > over) { + return MAX_VALUE; + } + if (d < -over) { + return Long.MIN_VALUE; + } + return d * m; + } + + public long toBytes(final long size) { + throw new AbstractMethodError(); + } + + public long toKilobytes(final long size) { + throw new AbstractMethodError(); + } + + public long toMegabytes(final long size) { + throw new AbstractMethodError(); + } + + public long toGigabytes(final long size) { + throw new AbstractMethodError(); + } + + public long toTerabytes(final long size) { + throw new AbstractMethodError(); + } + + public long convert(final long sourceSize, final SizeUnit sourceUnit) { + throw new AbstractMethodError(); + } + } +}