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