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        private final ReadWriteLock listenersLock = new ReentrantReadWriteLock();
067    
068        private final Queue<StatusData> messages = new BoundedQueue<StatusData>(MAX_ENTRIES);
069        private final Lock msgLock = new ReentrantLock();
070    
071        private int listenersLevel;
072    
073        private StatusLogger() {
074            this.logger = new SimpleLogger("StatusLogger", Level.ERROR, false, true, false, false, Strings.EMPTY, null, PROPS,
075                System.err);
076            this.listenersLevel = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel();
077        }
078    
079        /**
080         * Retrieve the StatusLogger.
081         * @return The StatusLogger.
082         */
083        public static StatusLogger getLogger() {
084            return STATUS_LOGGER;
085        }
086    
087        public void setLevel(final Level level) {
088            logger.setLevel(level);
089        }
090    
091        /**
092         * Register a new listener.
093         * @param listener The StatusListener to register.
094         */
095        public void registerListener(final StatusListener listener) {
096            listenersLock.writeLock().lock();
097            try {
098                listeners.add(listener);
099                final Level lvl = listener.getStatusLevel();
100                if (listenersLevel < lvl.intLevel()) {
101                    listenersLevel = lvl.intLevel();
102                }
103            } finally {
104                listenersLock.writeLock().unlock();
105            }
106        }
107    
108        /**
109         * Remove a StatusListener.
110         * @param listener The StatusListener to remove.
111         */
112        public void removeListener(final StatusListener listener) {
113            closeSilently(listener);
114            listenersLock.writeLock().lock();
115            try {
116                listeners.remove(listener);
117                int lowest = Level.toLevel(DEFAULT_STATUS_LEVEL, Level.WARN).intLevel();
118                for (final StatusListener l : listeners) {
119                    final int level = l.getStatusLevel().intLevel();
120                    if (lowest < level) {
121                        lowest = level;
122                    }
123                }
124                listenersLevel = lowest;
125            } finally {
126                listenersLock.writeLock().unlock();
127            }
128        }
129    
130        /**
131         * Returns a thread safe Iterable for the StatusListener.
132         * @return An Iterable for the list of StatusListeners.
133         */
134        public Iterable<StatusListener> getListeners() {
135            return listeners;
136        }
137    
138        /**
139         * Clears the list of status events and listeners.
140         */
141        public void reset() {
142            listenersLock.writeLock().lock();
143            try {
144                for (final StatusListener listener : listeners) {
145                    closeSilently(listener);
146                }
147            } finally {
148                listeners.clear();
149                listenersLock.writeLock().unlock();
150                // note this should certainly come after the unlock to avoid unnecessary nested locking
151                clear();
152            }
153        }
154    
155        private static void closeSilently(final Closeable resource) {
156            try {
157                resource.close();
158            } catch (final IOException ignored) {
159            }
160        }
161    
162        /**
163         * Returns a List of all events as StatusData objects.
164         * @return The list of StatusData objects.
165         */
166        public List<StatusData> getStatusData() {
167            msgLock.lock();
168            try {
169                return new ArrayList<StatusData>(messages);
170            } finally {
171                msgLock.unlock();
172            }
173        }
174    
175        /**
176         * Clears the list of status events.
177         */
178        public void clear() {
179            msgLock.lock();
180            try {
181                messages.clear();
182            } finally {
183                msgLock.unlock();
184            }
185        }
186    
187        @Override
188        public Level getLevel() {
189            return logger.getLevel();
190        }
191    
192        /**
193         * Add an event.
194         * @param marker The Marker
195         * @param fqcn   The fully qualified class name of the <b>caller</b>
196         * @param level  The logging level
197         * @param msg    The message associated with the event.
198         * @param t      A Throwable or null.
199         */
200        @Override
201        public void logMessage(final String fqcn, final Level level, final Marker marker, final Message msg, final Throwable t) {
202            StackTraceElement element = null;
203            if (fqcn != null) {
204                element = getStackTraceElement(fqcn, Thread.currentThread().getStackTrace());
205            }
206            final StatusData data = new StatusData(element, level, msg, t);
207            msgLock.lock();
208            try {
209                messages.add(data);
210            } finally {
211                msgLock.unlock();
212            }
213            if (listeners.size() > 0) {
214                for (final StatusListener listener : listeners) {
215                    if (data.getLevel().isMoreSpecificThan(listener.getStatusLevel())) {
216                        listener.log(data);
217                    }
218                }
219            } else {
220                logger.logMessage(fqcn, level, marker, msg, t);
221            }
222        }
223    
224        private StackTraceElement getStackTraceElement(final String fqcn, final StackTraceElement[] stackTrace) {
225            if (fqcn == null) {
226                return null;
227            }
228            boolean next = false;
229            for (final StackTraceElement element : stackTrace) {
230                final String className = element.getClassName();
231                if (next && !fqcn.equals(className)) {
232                    return element;
233                }
234                if (fqcn.equals(className)) {
235                    next = true;
236                } else if (NOT_AVAIL.equals(className)) {
237                    break;
238                }
239            }
240            return null;
241        }
242    
243        @Override
244        public boolean isEnabled(final Level level, final Marker marker, final String message, final Throwable t) {
245            return isEnabled(level, marker);
246        }
247    
248        @Override
249        public boolean isEnabled(final Level level, final Marker marker, final String message) {
250            return isEnabled(level, marker);
251        }
252    
253        @Override
254        public boolean isEnabled(final Level level, final Marker marker, final String message, final Object... params) {
255            return isEnabled(level, marker);
256        }
257    
258        @Override
259        public boolean isEnabled(final Level level, final Marker marker, final Object message, final Throwable t) {
260            return isEnabled(level, marker);
261        }
262    
263        @Override
264        public boolean isEnabled(final Level level, final Marker marker, final Message message, final Throwable t) {
265            return isEnabled(level, marker);
266        }
267    
268        @Override
269        public boolean isEnabled(final Level level, final Marker marker) {
270            if (listeners.size() > 0) {
271                return listenersLevel >= level.intLevel();
272            }
273            return logger.isEnabled(level, marker);
274        }
275    
276        /**
277         * Queue for status events.
278         * @param <E> Object type to be stored in the queue.
279         */
280        private class BoundedQueue<E> extends ConcurrentLinkedQueue<E> {
281    
282            private static final long serialVersionUID = -3945953719763255337L;
283    
284            private final int size;
285    
286            public BoundedQueue(final int size) {
287                this.size = size;
288            }
289    
290            @Override
291            public boolean add(final E object) {
292                while (messages.size() > size) {
293                    messages.poll();
294                }
295                return super.add(object);
296            }
297        }
298    }