|
@@ -0,0 +1,358 @@
|
|
|
+/**
|
|
|
+ * 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|