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