| /* |
| * 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.sis.util.logging; |
| |
| import java.util.ServiceLoader; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import java.util.logging.LogRecord; |
| |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.Configuration; |
| import org.apache.sis.util.Static; |
| import org.apache.sis.util.Exceptions; |
| import org.apache.sis.util.Classes; |
| import org.apache.sis.internal.jdk9.JDK9; |
| import org.apache.sis.internal.system.Modules; |
| |
| |
| /** |
| * A set of utilities method for configuring loggings in SIS. Library implementers should fetch |
| * their loggers using the {@link #getLogger(String)} static method defined in this {@code Logging} |
| * class rather than the one defined in the standard {@link Logger} class, in order to give SIS a |
| * chance to redirect the logs to an other framework like |
| * <a href="http://commons.apache.org/logging/">Commons-logging</a> or |
| * <a href="http://logging.apache.org/log4j">Log4J</a>. |
| * |
| * <p>This class provides also some convenience static methods, including:</p> |
| * <ul> |
| * <li>{@link #log(Class, String, LogRecord)} for {@linkplain LogRecord#setLoggerName(String) setting |
| * the logger name}, {@linkplain LogRecord#setSourceClassName(String) source class name} and |
| * {@linkplain LogRecord#setSourceMethodName(String) source method name} of the given record |
| * before to log it.</li> |
| * <li>{@link #unexpectedException(Logger, Class, String, Throwable)} for reporting an anomalous but |
| * nevertheless non-fatal exception.</li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 1.1 |
| * @since 0.3 |
| * @module |
| */ |
| public final class Logging extends Static { |
| /** |
| * The threshold at which {@link #unexpectedException(Logger, String, String, Throwable, Level)} shall |
| * set the throwable in the {@link LogRecord}. For any record to be logged at a lower {@link Level}, |
| * the {@link LogRecord#setThrown(Throwable)} method will not be invoked. |
| * |
| * <p>The default value is 600, which is the {@link PerformanceLevel#PERFORMANCE} value. |
| * This value is between {@link Level#FINE} (500) and {@link Level#CONFIG} (700). |
| * Consequently we will ignore the stack traces of recoverable failures, but will report |
| * stack traces that may impact performance, configuration, or correctness.</p> |
| */ |
| private static final int LEVEL_THRESHOLD_FOR_STACKTRACE = 600; |
| |
| /** |
| * The factory for obtaining {@link Logger} instances, or {@code null} if none. |
| * If {@code null} (the default), then the standard JDK logging framework will be used. |
| * {@code Logging} scans the classpath for logger factories on class initialization. |
| * The fully qualified factory classname shall be declared in the following file: |
| * |
| * {@preformat text |
| * META-INF/services/org.apache.sis.util.logging.LoggerFactory |
| * } |
| * |
| * The factory found on the classpath is assigned to the {@link #factory} field. If more than one factory |
| * is found, then the log messages will be sent to the logging frameworks managed by all those factories. |
| * |
| * <div class="note"><b>API note:</b> |
| * A previous version was providing a {@code scanForPlugins()} method allowing developers to refresh the |
| * object state when new {@link LoggerFactory} instances become available on the classpath of a running JVM. |
| * However it usually doesn't work since loggers are typically stored in static final fields.</div> |
| * |
| * @see #setLoggerFactory(LoggerFactory) |
| */ |
| private static volatile LoggerFactory<?> factory; |
| static { |
| /* |
| * Use ServiceLoader.load(…), not DefaultFactories.createServiceLoader(…), for avoiding a never-ending |
| * loop if a warning occurs in DefaultFactories. This risk exists because DefaultFactories may use the |
| * logging services. Anyway, Apache SIS does not define any custom logger factory, so DefaultFactories |
| * is not needed in this case. |
| */ |
| LoggerFactory<?> factory = null; |
| for (final LoggerFactory<?> found : ServiceLoader.load(LoggerFactory.class)) { |
| if (factory == null) { |
| factory = found; |
| } else { |
| factory = new DualLoggerFactory(factory, found); |
| } |
| } |
| Logging.factory = factory; |
| } |
| |
| /** |
| * Do not allow instantiation of this class. |
| */ |
| private Logging() { |
| } |
| |
| /** |
| * Sets a new factory to use for obtaining {@link Logger} instances. |
| * If the given {@code factory} argument is {@code null} (the default), |
| * then the standard Logging framework will be used. |
| * |
| * <h4>Limitation</h4> |
| * SIS classes typically declare a logger constant like below: |
| * |
| * {@preformat java |
| * public static final Logger LOGGER = Logging.getLogger("the.logger.name"); |
| * } |
| * |
| * Factory changes will take effect only if this method is invoked before the initialization |
| * of such classes. |
| * |
| * @param factory the new logger factory, or {@code null} if none. |
| */ |
| @Configuration |
| public static void setLoggerFactory(final LoggerFactory<?> factory) { |
| Logging.factory = factory; |
| } |
| |
| /** |
| * Returns the factory used for obtaining {@link Logger} instances, or {@code null} if none. |
| * |
| * @return the current logger factory, or {@code null} if none. |
| */ |
| public static LoggerFactory<?> getLoggerFactory() { |
| return factory; |
| } |
| |
| /** |
| * Returns a logger for the specified name. If a {@linkplain LoggerFactory logger factory} has been set, |
| * then this method first {@linkplain LoggerFactory#getLogger(String) asks to the factory}. |
| * This rule gives SIS a chance to redirect logging events to |
| * <a href="http://commons.apache.org/logging/">commons-logging</a> or some equivalent framework. |
| * Only if no factory was found or if the factory choose to not redirect the loggings, then this |
| * method delegate to <code>{@linkplain Logger#getLogger(String) Logger.getLogger}(name)</code>. |
| * |
| * @param name the logger name. |
| * @return a logger for the specified name. |
| */ |
| public static Logger getLogger(final String name) { |
| ArgumentChecks.ensureNonNull("name", name); |
| final LoggerFactory<?> factory = Logging.factory; |
| if (factory != null) { |
| final Logger logger = factory.getLogger(name); |
| if (logger != null) { |
| return logger; |
| } |
| } |
| return Logger.getLogger(name); |
| } |
| |
| /** |
| * Returns a logger for the package of the specified class. This convenience method invokes |
| * {@link #getLogger(String)} with the package name of the given class taken as the logger name. |
| * |
| * @param source the class which will emit a logging message. |
| * @return a logger for the specified class. |
| * |
| * @since 1.0 |
| */ |
| public static Logger getLogger(final Class<?> source) { |
| ArgumentChecks.ensureNonNull("source", source); |
| String name = JDK9.getPackageName(source); |
| if (name.startsWith(Modules.INTERNAL_CLASSNAME_PREFIX)) { |
| // Remove the "internal" part from Apache SIS package names. |
| name = Modules.CLASSNAME_PREFIX + name.substring(Modules.INTERNAL_CLASSNAME_PREFIX.length()); |
| } |
| return getLogger(name); |
| } |
| |
| /** |
| * Logs the given record to the logger associated to the given class. |
| * This convenience method performs the following steps: |
| * |
| * <ul> |
| * <li>Unconditionally {@linkplain LogRecord#setSourceClassName(String) set the source class name} |
| * to the {@linkplain Class#getCanonicalName() canonical name} of the given class;</li> |
| * <li>Unconditionally {@linkplain LogRecord#setSourceMethodName(String) set the source method name} |
| * to the given value;</li> |
| * <li>Get the logger for the {@linkplain LogRecord#getLoggerName() logger name} if specified, |
| * or for the {@code classe} package name otherwise;</li> |
| * <li>{@linkplain LogRecord#setLoggerName(String) Set the logger name} of the given record, |
| * if not already set;</li> |
| * <li>{@linkplain Logger#log(LogRecord) Log} the modified record.</li> |
| * </ul> |
| * |
| * @param classe the class for which to obtain a logger. |
| * @param method the name of the method which is logging a record. |
| * @param record the record to log. |
| */ |
| public static void log(final Class<?> classe, String method, final LogRecord record) { |
| ArgumentChecks.ensureNonNull("record", record); |
| final String loggerName = record.getLoggerName(); |
| Logger logger; |
| if (loggerName == null) { |
| logger = getLogger(classe); |
| record.setLoggerName(logger.getName()); |
| } else { |
| logger = getLogger(loggerName); |
| } |
| if (classe != null && method != null) { |
| record.setSourceClassName(classe.getCanonicalName()); |
| record.setSourceMethodName(method); |
| } else { |
| /* |
| * If the given class or method is null, infer them from stack trace. We do not document this feature |
| * in public API because the rules applied here are heuristic and may change in any future SIS version. |
| */ |
| logger = inferCaller(logger, (classe != null) ? classe.getCanonicalName() : null, |
| method, Thread.currentThread().getStackTrace(), record); |
| } |
| logger.log(record); |
| } |
| |
| /** |
| * Sets the {@code LogRecord} source class and method names according values inferred from the given stack trace. |
| * This method inspects the given stack trace, skips what looks like internal API based on heuristic rules, then |
| * if some arguments are non-null tries to match them. |
| * |
| * @param logger where the log record will be sent after this method call, or {@code null} if unknown. |
| * @param classe the name of the class to report in the log record, or {@code null} if unknown. |
| * @param method the name of the method to report in the log record, or {@code null} if unknown. |
| * @param trace the stack trace to use for inferring the class and method names. |
| * @param record the record where to set the class and method names. |
| * @return the record to use for logging the record. |
| */ |
| private static Logger inferCaller(Logger logger, String classe, String method, |
| final StackTraceElement[] trace, final LogRecord record) |
| { |
| for (final StackTraceElement element : trace) { |
| /* |
| * Search for the first stack trace element with a classname matching the expected one. |
| * We compare against the name of the class given in argument if it was non-null. |
| * |
| * Note: a previous version also compared logger name with package name. |
| * This has been removed because those names are only loosely related. |
| */ |
| final String classname = element.getClassName(); |
| if (classe != null) { |
| if (!classname.equals(classe)) { |
| continue; |
| } |
| } else if (!isPublic(element)) { |
| continue; |
| } |
| /* |
| * Now that we have a stack trace element from the expected class (or any |
| * element if we don't know the class), make sure that we have the right method. |
| */ |
| final String methodName = element.getMethodName(); |
| if (method != null && !methodName.equals(method)) { |
| continue; |
| } |
| /* |
| * Now computes every values that are null, and stop the loop. |
| */ |
| if (logger == null) { |
| final int separator = classname.lastIndexOf('.'); |
| logger = getLogger((separator >= 1) ? classname.substring(0, separator-1) : ""); |
| } |
| if (classe == null) { |
| classe = classname; |
| } |
| if (method == null) { |
| method = methodName; |
| } |
| break; |
| } |
| /* |
| * The logger may stay null if we have been unable to find a suitable stack trace. |
| * Fallback on the global logger. |
| */ |
| if (logger == null) { |
| logger = getLogger(Logger.GLOBAL_LOGGER_NAME); |
| } |
| if (classe != null) { |
| record.setSourceClassName(classe); |
| } |
| if (method != null) { |
| record.setSourceMethodName(method); |
| } |
| return logger; |
| } |
| |
| /** |
| * Returns {@code true} if the given stack trace element describes a method considered part of public API. |
| * This method is invoked in order to infer the class and method names to declare in a {@link LogRecord}. |
| * We do not document this feature in public Javadoc because it is based on heuristic rules that may change. |
| * |
| * <p>The current implementation compares the class name against a hard-coded list of classes to hide. |
| * This implementation may change in any future SIS version.</p> |
| * |
| * @param e a stack trace element. |
| * @return {@code true} if the class and method specified by the given element can be considered public API. |
| */ |
| private static boolean isPublic(final StackTraceElement e) { |
| final String classname = e.getClassName(); |
| if (classname.startsWith("java") || classname.startsWith(Modules.INTERNAL_CLASSNAME_PREFIX) || |
| classname.indexOf('$') >= 0 || e.getMethodName().indexOf('$') >= 0) |
| { |
| return false; |
| } |
| if (classname.startsWith(Modules.CLASSNAME_PREFIX + "util.logging.")) { |
| return classname.endsWith("Test"); // Consider JUnit tests as public. |
| } |
| return true; // TODO: with StackWalker on JDK9, check if the class is public. |
| } |
| |
| /** |
| * Invoked when an unexpected error occurred. This method logs a message at {@link Level#WARNING} |
| * to the specified logger. The originating class name and method name can optionally be specified. |
| * If any of them is {@code null}, then it will be inferred from the error stack trace as described below. |
| * |
| * <div class="note"><b>Recommended usage:</b> |
| * explicit value for class and method names are preferred to automatic inference for the following reasons: |
| * <ul> |
| * <li>Automatic inference is not 100% reliable, since the Java Virtual Machine |
| * is free to omit stack frame in optimized code.</li> |
| * <li>When an exception occurred in a private method used internally by a public |
| * method, we sometime want to log the warning for the public method instead, |
| * since the user is not expected to know anything about the existence of the |
| * private method. If a developer really want to know about the private method, |
| * the stack trace is still available anyway.</li> |
| * </ul></div> |
| * |
| * If the {@code classe} or {@code method} arguments are null, then the originating class name and method name |
| * are inferred from the given {@code error} using the first {@linkplain StackTraceElement stack trace element} |
| * for which the class name is inside a package or sub-package of the same name than the logger name. |
| * |
| * <div class="note"><b>Example:</b> |
| * if the logger name is {@code "org.apache.sis.image"}, then this method will uses the first stack |
| * trace element where the fully qualified class name starts with {@code "org.apache.sis.image"} or |
| * {@code "org.apache.sis.image.io"}, but not {@code "org.apache.sis.imageio"}.</div> |
| * |
| * @param logger where to log the error, or {@code null} for inferring a default value from other arguments. |
| * @param classe the class where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param method the method where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param error the error, or {@code null} if none. |
| * @return {@code true} if the error has been logged, or {@code false} if the given {@code error} |
| * was null or if the logger does not log anything at {@link Level#WARNING}. |
| * |
| * @see #recoverableException(Logger, Class, String, Throwable) |
| * @see #severeException(Logger, Class, String, Throwable) |
| */ |
| public static boolean unexpectedException(final Logger logger, final Class<?> classe, |
| final String method, final Throwable error) |
| { |
| final String classname = (classe != null) ? classe.getName() : null; |
| return unexpectedException(logger, classname, method, error, Level.WARNING); |
| } |
| |
| /** |
| * Implementation of {@link #unexpectedException(Logger, Class, String, Throwable)}. |
| * |
| * @param logger where to log the error, or {@code null} for inferring a default value from other arguments. |
| * @param classe the fully qualified class name where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param method the method where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param error the error, or {@code null} if none. |
| * @param level the logging level. |
| * @return {@code true} if the error has been logged, or {@code false} if the given {@code error} |
| * was null or if the logger does not log anything at the specified level. |
| */ |
| private static boolean unexpectedException(Logger logger, String classe, String method, |
| final Throwable error, final Level level) |
| { |
| /* |
| * Checks if loggable, inferring the logger from the classe name if needed. |
| */ |
| if (error == null) { |
| return false; |
| } |
| if (logger == null && classe != null) { |
| final int separator = classe.lastIndexOf('.'); |
| final String paquet = (separator >= 1) ? classe.substring(0, separator-1) : ""; |
| logger = getLogger(paquet); |
| } |
| if (logger != null && !logger.isLoggable(level)) { |
| return false; |
| } |
| /* |
| * The message is fetched using Exception.getMessage() instead than getLocalizedMessage() |
| * because in a client-server architecture, we want the locale on the server-side instead |
| * than the locale on the client side. See LocalizedException policy. |
| */ |
| final StringBuilder buffer = new StringBuilder(256).append(Classes.getShortClassName(error)); |
| String message = error.getMessage(); // Targeted to system administrators (see above). |
| if (message != null) { |
| buffer.append(": ").append(message); |
| } |
| message = buffer.toString(); |
| message = Exceptions.formatChainedMessages(null, message, error); |
| final LogRecord record = new LogRecord(level, message); |
| if (level.intValue() >= LEVEL_THRESHOLD_FOR_STACKTRACE) { |
| record.setThrown(error); |
| } |
| if (logger == null || classe == null || method == null) { |
| logger = inferCaller(logger, classe, method, error.getStackTrace(), record); |
| } else { |
| record.setSourceClassName(classe); |
| record.setSourceMethodName(method); |
| } |
| record.setLoggerName(logger.getName()); |
| logger.log(record); |
| return true; |
| } |
| |
| /** |
| * Invoked when an unexpected error occurred while configuring the system. The error shall not |
| * prevent the application from working, but may change the behavior in some minor aspects. |
| * |
| * <div class="note"><b>Example:</b> |
| * If the {@code org.apache.sis.util.logging.MonolineFormatter.time} pattern declared in the |
| * {@code jre/lib/logging.properties} file is illegal, then {@link MonolineFormatter} will log |
| * this problem and use a default time pattern.</div> |
| * |
| * @param logger where to log the error, or {@code null} for inferring a default value from other arguments. |
| * @param classe the class where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param method the method name where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param error the error, or {@code null} if none. |
| * @return {@code true} if the error has been logged, or {@code false} if the given {@code error} |
| * was null or if the logger does not log anything at {@link Level#CONFIG}. |
| * |
| * @see #unexpectedException(Logger, Class, String, Throwable) |
| */ |
| static boolean configurationException(final Logger logger, final Class<?> classe, final String method, final Throwable error) { |
| final String classname = (classe != null) ? classe.getName() : null; |
| return unexpectedException(logger, classname, method, error, Level.CONFIG); |
| } |
| |
| /** |
| * Invoked when a recoverable error occurred. This method is similar to |
| * {@link #unexpectedException(Logger,Class,String,Throwable) unexpectedException(…)} |
| * except that it does not log the stack trace and uses a lower logging level. |
| * |
| * @param logger where to log the error, or {@code null} for inferring a default value from other arguments. |
| * @param classe the class where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param method the method name where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param error the error, or {@code null} if none. |
| * @return {@code true} if the error has been logged, or {@code false} if the given {@code error} |
| * was null or if the logger does not log anything at {@link Level#FINE}. |
| * |
| * @see #unexpectedException(Logger, Class, String, Throwable) |
| * @see #severeException(Logger, Class, String, Throwable) |
| */ |
| public static boolean recoverableException(final Logger logger, final Class<?> classe, |
| final String method, final Throwable error) |
| { |
| final String classname = (classe != null) ? classe.getName() : null; |
| return unexpectedException(logger, classname, method, error, Level.FINE); |
| } |
| |
| /** |
| * Invoked when an ignorable error occurred. This method is similar to |
| * {@link #recoverableException(Logger,Class,String,Throwable) unexpectedException(…)} |
| * except that it uses a lower logging level. |
| * |
| * @param logger where to log the error, or {@code null} for inferring a default value from other arguments. |
| * @param classe the class where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param method the method name where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param error the error, or {@code null} if none. |
| * @return {@code true} if the error has been logged, or {@code false} if the given {@code error} |
| * was null or if the logger does not log anything at {@link Level#FINER}. |
| * |
| * @since 1.0 |
| */ |
| public static boolean ignorableException(final Logger logger, final Class<?> classe, |
| final String method, final Throwable error) |
| { |
| final String classname = (classe != null) ? classe.getName() : null; |
| return unexpectedException(logger, classname, method, error, Level.FINER); |
| } |
| |
| /** |
| * Invoked when a severe error occurred. This method is similar to |
| * {@link #unexpectedException(Logger,Class,String,Throwable) unexpectedException} |
| * except that it logs the message at the {@link Level#SEVERE SEVERE} level. |
| * |
| * @param logger where to log the error, or {@code null} for inferring a default value from other arguments. |
| * @param classe the class where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param method the method name where the error occurred, or {@code null} for inferring a default value from other arguments. |
| * @param error the error, or {@code null} if none. |
| * @return {@code true} if the error has been logged, or {@code false} if the given {@code error} |
| * was null or if the logger does not log anything at {@link Level#SEVERE}. |
| * |
| * @see #unexpectedException(Logger, Class, String, Throwable) |
| * @see #recoverableException(Logger, Class, String, Throwable) |
| */ |
| public static boolean severeException(final Logger logger, final Class<?> classe, |
| final String method, final Throwable error) |
| { |
| final String classname = (classe != null) ? classe.getName() : null; |
| return unexpectedException(logger, classname, method, error, Level.SEVERE); |
| } |
| } |