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