001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements. See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache license, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License. You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the license for the specific language governing permissions and
015     * limitations under the license.
016     */
017    package org.apache.logging.log4j.status;
018    
019    import java.io.Closeable;
020    import java.io.IOException;
021    import java.util.ArrayList;
022    import java.util.Collection;
023    import java.util.List;
024    import java.util.Queue;
025    import java.util.concurrent.ConcurrentLinkedQueue;
026    import java.util.concurrent.CopyOnWriteArrayList;
027    import java.util.concurrent.locks.Lock;
028    import java.util.concurrent.locks.ReadWriteLock;
029    import java.util.concurrent.locks.ReentrantLock;
030    import java.util.concurrent.locks.ReentrantReadWriteLock;
031    
032    import org.apache.logging.log4j.Level;
033    import org.apache.logging.log4j.Marker;
034    import org.apache.logging.log4j.message.Message;
035    import org.apache.logging.log4j.simple.SimpleLogger;
036    import org.apache.logging.log4j.spi.AbstractLogger;
037    import org.apache.logging.log4j.util.PropertiesUtil;
038    import org.apache.logging.log4j.util.Strings;
039    
040    /**
041     * Mechanism to record events that occur in the logging system.
042     */
043    public final class StatusLogger extends AbstractLogger {
044    
045        private static final long serialVersionUID = 2L;
046    
047        /**
048         * System property that can be configured with the number of entries in the queue. Once the limit
049         * is reached older entries will be removed as new entries are added.
050         */
051        public static final String MAX_STATUS_ENTRIES = "log4j2.status.entries";
052    
053        private static final String NOT_AVAIL = "?";
054    
055        private static final PropertiesUtil PROPS = new PropertiesUtil("log4j2.StatusLogger.properties");
056    
057        private static final int MAX_ENTRIES = PROPS.getIntegerProperty(MAX_STATUS_ENTRIES, 200);
058    
059        private static final String DEFAULT_STATUS_LEVEL = PROPS.getStringProperty("log4j2.StatusLogger.level");
060    
061        private static final StatusLogger STATUS_LOGGER = new StatusLogger();
062    
063        private final SimpleLogger logger;
064    
065        private final Collection<StatusListener> listeners = new CopyOnWriteArrayList<StatusListener>();
066        @SuppressWarnings("NonSerializableFieldInSerializableClass") // ReentrantReadWriteLock is Serializable
067        private final ReadWriteLock listenersLock = new ReentrantReadWriteLock();
068    
069        private final Queue<StatusData> messages = new BoundedQueue<StatusData>(MAX_ENTRIES);
070        @SuppressWarnings("NonSerializableFieldInSerializableClass") // ReentrantLock is Serializable
071        private final Lock msgLock = new ReentrantLock();
072    
073        private int listenersLevel;
074    
075        private StatusLogger() {
076            this.logger = new SimpleLogger("StatusLogger", Level.ERROR, false, true, false, false, Strings.EMPTY, null, PROPS,
077                System.err);
078            this.listenersLevel = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel();
079        }
080    
081        /**
082         * Retrieve the StatusLogger.
083         * @return The StatusLogger.
084         */
085        public static StatusLogger getLogger() {
086            return STATUS_LOGGER;
087        }
088    
089        public void setLevel(final Level level) {
090            logger.setLevel(level);
091        }
092    
093        /**
094         * Register a new listener.
095         * @param listener The StatusListener to register.
096         */
097        public void registerListener(final StatusListener listener) {
098            listenersLock.writeLock().lock();
099            try {
100                listeners.add(listener);
101                final Level lvl = listener.getStatusLevel();
102                if (listenersLevel < lvl.intLevel()) {
103                    listenersLevel = lvl.intLevel();
104                }
105            } finally {
106                listenersLock.writeLock().unlock();
107            }
108        }
109    
110        /**
111         * Remove a StatusListener.
112         * @param listener The StatusListener to remove.
113         */
114        public void removeListener(final StatusListener listener) {
115            closeSilently(listener);
116            listenersLock.writeLock().lock();
117            try {
118                listeners.remove(listener);
119                int lowest = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel();
120                for (final StatusListener l : listeners) {
121                    final int level = l.getStatusLevel().intLevel();
122                    if (lowest < level) {
123                        lowest = level;
124                    }
125                }
126                listenersLevel = lowest;
127            } finally {
128                listenersLock.writeLock().unlock();
129            }
130        }
131    
132        /**
133         * Returns a thread safe Iterable for the StatusListener.
134         * @return An Iterable for the list of StatusListeners.
135         */
136        public Iterable<StatusListener> getListeners() {
137            return listeners;
138        }
139    
140        /**
141         * Clears the list of status events and listeners.
142         */
143        public void reset() {
144            listenersLock.writeLock().lock();
145            try {
146                for (final StatusListener listener : listeners) {
147                    closeSilently(listener);
148                }
149            } finally {
150                listeners.clear();
151                listenersLock.writeLock().unlock();
152                // note this should certainly come after the unlock to avoid unnecessary nested locking
153                clear();
154            }
155        }
156    
157        private static void closeSilently(final Closeable resource) {
158            try {
159                resource.close();
160            } catch (final IOException ignored) {
161            }
162        }
163    
164        /**
165         * Returns a List of all events as StatusData objects.
166         * @return The list of StatusData objects.
167         */
168        public List<StatusData> getStatusData() {
169            msgLock.lock();
170            try {
171                return new ArrayList<StatusData>(messages);
172            } finally {
173                msgLock.unlock();
174            }
175        }
176    
177        /**
178         * Clears the list of status events.
179         */
180        public void clear() {
181            msgLock.lock();
182            try {
183                messages.clear();
184            } finally {
185                msgLock.unlock();
186            }
187        }
188    
189        @Override
190        public Level getLevel() {
191            return logger.getLevel();
192        }
193    
194        /**
195         * Add an event.
196         * @param marker The Marker
197         * @param fqcn   The fully qualified class name of the <b>caller</b>
198         * @param level  The logging level
199         * @param msg    The message associated with the event.
200         * @param t      A Throwable or null.
201         */
202        @Override
203        public void logMessage(final String fqcn, final Level level, final Marker marker, final Message msg, final Throwable t) {
204            StackTraceElement element = null;
205            if (fqcn != null) {
206                element = getStackTraceElement(fqcn, Thread.currentThread().getStackTrace());
207            }
208            final StatusData data = new StatusData(element, level, msg, t);
209            msgLock.lock();
210            try {
211                messages.add(data);
212            } finally {
213                msgLock.unlock();
214            }
215            if (listeners.size() > 0) {
216                for (final StatusListener listener : listeners) {
217                    if (data.getLevel().isMoreSpecificThan(listener.getStatusLevel())) {
218                        listener.log(data);
219                    }
220                }
221            } else {
222                logger.logMessage(fqcn, level, marker, msg, t);
223            }
224        }
225    
226        private StackTraceElement getStackTraceElement(final String fqcn, final StackTraceElement[] stackTrace) {
227            if (fqcn == null) {
228                return null;
229            }
230            boolean next = false;
231            for (final StackTraceElement element : stackTrace) {
232                final String className = element.getClassName();
233                if (next && !fqcn.equals(className)) {
234                    return element;
235                }
236                if (fqcn.equals(className)) {
237                    next = true;
238                } else if (NOT_AVAIL.equals(className)) {
239                    break;
240                }
241            }
242            return null;
243        }
244    
245        @Override
246        public boolean isEnabled(final Level level, final Marker marker, final String message, final Throwable t) {
247            return isEnabled(level, marker);
248        }
249    
250        @Override
251        public boolean isEnabled(final Level level, final Marker marker, final String message) {
252            return isEnabled(level, marker);
253        }
254    
255        @Override
256        public boolean isEnabled(final Level level, final Marker marker, final String message, final Object... params) {
257            return isEnabled(level, marker);
258        }
259    
260        @Override
261        public boolean isEnabled(final Level level, final Marker marker, final Object message, final Throwable t) {
262            return isEnabled(level, marker);
263        }
264    
265        @Override
266        public boolean isEnabled(final Level level, final Marker marker, final Message message, final Throwable t) {
267            return isEnabled(level, marker);
268        }
269    
270        @Override
271        public boolean isEnabled(final Level level, final Marker marker) {
272            if (listeners.size() > 0) {
273                return listenersLevel >= level.intLevel();
274            }
275            return logger.isEnabled(level, marker);
276        }
277    
278        /**
279         * Queue for status events.
280         * @param <E> Object type to be stored in the queue.
281         */
282        private class BoundedQueue<E> extends ConcurrentLinkedQueue<E> {
283    
284            private static final long serialVersionUID = -3945953719763255337L;
285    
286            private final int size;
287    
288            public BoundedQueue(final int size) {
289                this.size = size;
290            }
291    
292            @Override
293            public boolean add(final E object) {
294                while (messages.size() > size) {
295                    messages.poll();
296                }
297                return super.add(object);
298            }
299        }
300    }