blob: 6190e0ccf98d814353d4c940943ebd0798d32ac1 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.log;
import com.google.common.annotations.VisibleForTesting;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.math3.stat.descriptive.SummaryStatistics;
import org.apache.hadoop.util.Timer;
/**
* This is a class to help easily throttle log statements, so that they will
* not be emitted more frequently than a certain rate. It is useful to help
* prevent flooding the application logs with redundant messages.
*
* The instantiator specifies a minimum period at which statements should be
* logged. When {@link #record(double...)} is called, if enough time has elapsed
* since the last time it was called, the return value will indicate to the
* caller that it should write to its actual log. Note that this class does not
* write to any actual log; it only records information about how many times
* {@code record} has been called and with what arguments, and indicates to the
* caller whether or not it should write to its log. If not enough time has yet
* elapsed, this class records the arguments and updates its summary
* information, and indicates to the caller that it should not log.
*
* For example, say that you want to know whenever too large of a request is
* received, but want to avoid flooding the logs if many such requests are
* received.
* <pre>{@code
* // Helper with a minimum period of 5 seconds
* private LogThrottlingHelper helper = new LogThrottlingHelper(5000);
*
* public void receiveRequest(int requestedObjects) {
* if (requestedObjects > MAXIMUM_REQUEST_SIZE) {
* LogAction logAction = helper.record(requestedObjects);
* if (logAction.shouldLog()) {
* LOG.warn("Received {} large request(s) with a total of {} objects " +
* "requested; maximum objects requested was {}",
* logAction.getCount(), logAction.getStats(0).getSum(),
* logAction.getStats(0).getMax());
* }
* }
* }
* }</pre>
* The above snippet allows you to record extraneous events, but if they become
* frequent, to limit their presence in the log to only every 5 seconds while
* still maintaining overall information about how many large requests were
* received.
*
* <p/>This class can also be used to coordinate multiple logging points; see
* {@link #record(String, long, double...)} for more details.
*
* <p/>This class is not thread-safe.
*/
public class LogThrottlingHelper {
/**
* An indication of what action the caller should take. If
* {@link #shouldLog()} is false, no other action should be taken, and it is
* an error to try to access any of the summary information. If
* {@link #shouldLog()} is true, then the caller should write to its log, and
* can use the {@link #getCount()} and {@link #getStats(int)} methods to
* determine summary information about what has been recorded into this
* helper.
*
* All summary information in this action only represents
* {@link #record(double...)} statements which were called <i>after</i> the
* last time the caller logged something; that is, since the last time a log
* action was returned with a true value for {@link #shouldLog()}. Information
* about the {@link #record(double...)} statement which created this log
* action is included.
*/
public interface LogAction {
/**
* Return the number of records encapsulated in this action; that is, the
* number of times {@code record} was called to produce this action,
* including the current one.
*/
int getCount();
/**
* Return summary information for the value that was recorded at index
* {@code idx}. Corresponds to the ordering of values passed to
* {@link #record(double...)}.
*/
SummaryStatistics getStats(int idx);
/**
* If this is true, the caller should write to its log. Otherwise, the
* caller should take no action, and it is an error to call other methods
* on this object.
*/
boolean shouldLog();
}
/**
* A {@link LogAction} representing a state that should not yet be logged.
* If any attempt is made to extract information from this, it will throw
* an {@link IllegalStateException}.
*/
public static final LogAction DO_NOT_LOG = new NoLogAction();
private static final String DEFAULT_RECORDER_NAME =
"__DEFAULT_RECORDER_NAME__";
/**
* This throttler will not trigger log statements more frequently than this
* period.
*/
private final long minLogPeriodMs;
/**
* The name of the recorder treated as the primary; this is the only one which
* will trigger logging. Other recorders are dependent on the state of this
* recorder. This may be null, in which case a primary has not yet been set.
*/
private String primaryRecorderName;
private final Timer timer;
private final Map<String, LoggingAction> currentLogs;
private long lastLogTimestampMs = Long.MIN_VALUE;
/**
* Create a log helper without any primary recorder.
*
* @see #LogThrottlingHelper(long, String)
*/
public LogThrottlingHelper(long minLogPeriodMs) {
this(minLogPeriodMs, null);
}
/**
* Create a log helper with a specified primary recorder name; this can be
* used in conjunction with {@link #record(String, long, double...)} to set up
* primary and dependent recorders. See
* {@link #record(String, long, double...)} for more details.
*
* @param minLogPeriodMs The minimum period with which to log; do not log
* more frequently than this.
* @param primaryRecorderName The name of the primary recorder.
*/
public LogThrottlingHelper(long minLogPeriodMs, String primaryRecorderName) {
this(minLogPeriodMs, primaryRecorderName, new Timer());
}
@VisibleForTesting
LogThrottlingHelper(long minLogPeriodMs, String primaryRecorderName,
Timer timer) {
this.minLogPeriodMs = minLogPeriodMs;
this.primaryRecorderName = primaryRecorderName;
this.timer = timer;
this.currentLogs = new HashMap<>();
}
/**
* Record some set of values at the current time into this helper. Note that
* this does <i>not</i> actually write information to any log. Instead, this
* will return a LogAction indicating whether or not the caller should write
* to its own log. The LogAction will additionally contain summary information
* about the values specified since the last time the caller was expected to
* write to its log.
*
* <p/>Specifying multiple values will maintain separate summary statistics
* about each value. For example:
* <pre>{@code
* helper.record(1, 0);
* LogAction action = helper.record(3, 100);
* action.getStats(0); // == 2
* action.getStats(1); // == 50
* }</pre>
*
* @param values The values about which to maintain summary information. Every
* time this method is called, the same number of values must
* be specified.
* @return A LogAction indicating whether or not the caller should write to
* its log.
*/
public LogAction record(double... values) {
return record(DEFAULT_RECORDER_NAME, timer.monotonicNow(), values);
}
/**
* Record some set of values at the specified time into this helper. This can
* be useful to avoid fetching the current time twice if the caller has
* already done so for other purposes. This additionally allows the caller to
* specify a name for this recorder. When multiple names are used, one is
* denoted as the primary recorder. Only recorders named as the primary
* will trigger logging; other names not matching the primary can <i>only</i>
* be triggered by following the primary. This is used to coordinate multiple
* logging points. A primary can be set via the
* {@link #LogThrottlingHelper(long, String)} constructor. If no primary
* is set in the constructor, then the first recorder name used becomes the
* primary.
*
* If multiple names are used, they maintain entirely different sets of values
* and summary information. For example:
* <pre>{@code
* // Initialize "pre" as the primary recorder name
* LogThrottlingHelper helper = new LogThrottlingHelper(1000, "pre");
* LogAction preLog = helper.record("pre", Time.monotonicNow());
* if (preLog.shouldLog()) {
* // ...
* }
* double eventsProcessed = ... // perform some action
* LogAction postLog =
* helper.record("post", Time.monotonicNow(), eventsProcessed);
* if (postLog.shouldLog()) {
* // ...
* // Can use postLog.getStats(0) to access eventsProcessed information
* }
* }</pre>
* Since "pre" is the primary recorder name, logging to "pre" will trigger a
* log action if enough time has elapsed. This will indicate that "post"
* should log as well. This ensures that "post" is always logged in the same
* iteration as "pre", yet each one is able to maintain its own summary
* information.
*
* <p/>Other behavior is the same as {@link #record(double...)}.
*
* @param recorderName The name of the recorder. This is used to check if the
* current recorder is the primary. Other names are
* arbitrary and are only used to differentiate between
* distinct recorders.
* @param currentTimeMs The current time.
* @param values The values to log.
* @return The LogAction for the specified recorder.
*
* @see #record(double...)
*/
public LogAction record(String recorderName, long currentTimeMs,
double... values) {
if (primaryRecorderName == null) {
primaryRecorderName = recorderName;
}
LoggingAction currentLog = currentLogs.get(recorderName);
if (currentLog == null || currentLog.hasLogged()) {
currentLog = new LoggingAction(values.length);
if (!currentLogs.containsKey(recorderName)) {
// Always log newly created loggers
currentLog.setShouldLog();
}
currentLogs.put(recorderName, currentLog);
}
currentLog.recordValues(values);
if (primaryRecorderName.equals(recorderName) &&
currentTimeMs - minLogPeriodMs >= lastLogTimestampMs) {
lastLogTimestampMs = currentTimeMs;
for (LoggingAction log : currentLogs.values()) {
log.setShouldLog();
}
}
if (currentLog.shouldLog()) {
currentLog.setHasLogged();
return currentLog;
} else {
return DO_NOT_LOG;
}
}
/**
* Helper function to create a message about how many log statements were
* suppressed in the provided log action. If no statements were suppressed,
* this returns an empty string. The message has the format (without quotes):
*
* <p/>' (suppressed logging <i>{suppression_count}</i> times)'
*
* @param action The log action to produce a message about.
* @return A message about suppression within this action.
*/
public static String getLogSupressionMessage(LogAction action) {
if (action.getCount() > 1) {
return " (suppressed logging " + (action.getCount() - 1) + " times)";
} else {
return "";
}
}
/**
* A standard log action which keeps track of all of the values which have
* been logged. This is also used for internal bookkeeping via its private
* fields and methods; it will maintain whether or not it is ready to be
* logged ({@link #shouldLog()}) as well as whether or not it has been
* returned for logging yet ({@link #hasLogged()}).
*/
private static class LoggingAction implements LogAction {
private int count = 0;
private final SummaryStatistics[] stats;
private boolean shouldLog = false;
private boolean hasLogged = false;
LoggingAction(int valueCount) {
stats = new SummaryStatistics[valueCount];
for (int i = 0; i < stats.length; i++) {
stats[i] = new SummaryStatistics();
}
}
public int getCount() {
return count;
}
public SummaryStatistics getStats(int idx) {
if (idx < 0 || idx >= stats.length) {
throw new IllegalArgumentException("Requested stats at idx " + idx +
" but this log only maintains " + stats.length + " stats");
}
return stats[idx];
}
public boolean shouldLog() {
return shouldLog;
}
private void setShouldLog() {
shouldLog = true;
}
private boolean hasLogged() {
return hasLogged;
}
private void setHasLogged() {
hasLogged = true;
}
private void recordValues(double... values) {
if (values.length != stats.length) {
throw new IllegalArgumentException("received " + values.length +
" values but expected " + stats.length);
}
count++;
for (int i = 0; i < values.length; i++) {
stats[i].addValue(values[i]);
}
}
}
/**
* A non-logging action.
*
* @see #DO_NOT_LOG
*/
private static class NoLogAction implements LogAction {
public int getCount() {
throw new IllegalStateException("Cannot be logged yet!");
}
public SummaryStatistics getStats(int idx) {
throw new IllegalStateException("Cannot be logged yet!");
}
public boolean shouldLog() {
return false;
}
}
}