Repository: cassandra
Updated Branches:
  refs/heads/cassandra-2.1 739f3e37c -> e6f027979


Introduce NoSpamLogger

patch by ariel; reviewed by benedict for CASSANDRA-9029


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/5bffaf85
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/5bffaf85
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/5bffaf85

Branch: refs/heads/cassandra-2.1
Commit: 5bffaf850ca3e978baaa8664acc65612d7460d3f
Parents: 739f3e3
Author: Ariel Weisberg <ariel.wesib...@datastax.com>
Authored: Fri Apr 3 23:27:10 2015 +0100
Committer: T Jake Luciani <j...@apache.org>
Committed: Mon May 4 12:21:59 2015 -0400

----------------------------------------------------------------------
 .../apache/cassandra/utils/NoSpamLogger.java    | 238 +++++++++++++++++++
 .../cassandra/utils/NoSpamLoggerTest.java       | 174 ++++++++++++++
 2 files changed, 412 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/5bffaf85/src/java/org/apache/cassandra/utils/NoSpamLogger.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/utils/NoSpamLogger.java 
b/src/java/org/apache/cassandra/utils/NoSpamLogger.java
new file mode 100644
index 0000000..9f5d5ce
--- /dev/null
+++ b/src/java/org/apache/cassandra/utils/NoSpamLogger.java
@@ -0,0 +1,238 @@
+/*
+ * 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.cassandra.utils;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.cliffc.high_scale_lib.NonBlockingHashMap;
+import org.slf4j.Logger;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Logging that limits each log statement to firing based on time since the 
statement last fired.
+ *
+ * Every logger has a unique timer per statement. Minimum time between logging 
is set for each statement
+ * the first time it is used and a subsequent attempt to request that 
statement with a different minimum time will
+ * result in the original time being used. No warning is provided if there is 
a mismatch.
+ *
+ * If the statement is cached and used to log directly then only a volatile 
read will be required in the common case.
+ * If the Logger is cached then there is a single concurrent hash map lookup + 
the volatile read.
+ * If neither the logger nor the statement is cached then it is two concurrent 
hash map lookups + the volatile read.
+ *
+ */
+public class NoSpamLogger
+{
+    /**
+     * Levels for programmatically specifying the severity of a log statement
+     */
+    public enum Level
+    {
+        INFO, WARN, ERROR;
+    }
+
+    @VisibleForTesting
+    static interface Clock
+    {
+        long nanoTime();
+    }
+
+    @VisibleForTesting
+    static Clock CLOCK = new Clock()
+    {
+        public long nanoTime()
+        {
+            return System.nanoTime();
+        }
+    };
+
+    public class NoSpamLogStatement extends AtomicLong
+    {
+        private static final long serialVersionUID = 1L;
+
+        private final String statement;
+        private final long minIntervalNanos;
+
+        public NoSpamLogStatement(String statement, long minIntervalNanos)
+        {
+            this.statement = statement;
+            this.minIntervalNanos = minIntervalNanos;
+        }
+
+        private boolean shouldLog(long nowNanos)
+        {
+            long expected = get();
+            return nowNanos - expected >= minIntervalNanos && 
compareAndSet(expected, nowNanos);
+        }
+
+        public void log(Level l, long nowNanos, Object... objects)
+        {
+            if (!shouldLog(nowNanos)) return;
+
+            switch (l)
+            {
+            case INFO:
+                wrapped.info(statement, objects);
+                break;
+            case WARN:
+                wrapped.warn(statement, objects);
+                break;
+            case ERROR:
+                wrapped.error(statement, objects);
+                break;
+                default:
+                    throw new AssertionError();
+            }
+        }
+
+        public void info(long nowNanos, Object... objects)
+        {
+            log(Level.INFO, nowNanos, objects);
+        }
+
+        public void info(Object... objects)
+        {
+            info(CLOCK.nanoTime(), objects);
+        }
+
+        public void warn(long nowNanos, Object... objects)
+        {
+            log(Level.WARN, nowNanos, objects);
+        }
+
+        public void warn(String s, Object... objects)
+        {
+            warn(CLOCK.nanoTime(), s, objects);
+        }
+
+        public void error(long nowNanos, Object... objects)
+        {
+            log(Level.ERROR, nowNanos, objects);
+        }
+
+        public void error(Object... objects)
+        {
+            error(CLOCK.nanoTime(), objects);
+        }
+    }
+
+    private static final NonBlockingHashMap<Logger, NoSpamLogger> 
wrappedLoggers = new NonBlockingHashMap<>();
+
+    @VisibleForTesting
+    static void clearWrappedLoggersForTest()
+    {
+        wrappedLoggers.clear();
+    }
+
+    public static NoSpamLogger getLogger(Logger logger, long minInterval, 
TimeUnit unit)
+    {
+        NoSpamLogger wrapped = wrappedLoggers.get(logger);
+        if (wrapped == null)
+        {
+            wrapped = new NoSpamLogger(logger, minInterval, unit);
+            NoSpamLogger temp = wrappedLoggers.putIfAbsent(logger, wrapped);
+            if (temp != null)
+                wrapped = temp;
+        }
+        return wrapped;
+    }
+
+    public static void log(Logger logger, Level level, long minInterval, 
TimeUnit unit, String message, Object... objects)
+    {
+        log(logger, level, minInterval, unit, CLOCK.nanoTime(), message, 
objects);
+    }
+
+    public static void log(Logger logger, Level level, long minInterval, 
TimeUnit unit, long nowNanos, String message, Object... objects)
+    {
+        NoSpamLogger wrapped = getLogger(logger, minInterval, unit);
+        NoSpamLogStatement statement = wrapped.getStatement(message);
+        statement.log(level, nowNanos, objects);
+    }
+
+    public static NoSpamLogStatement getStatement(Logger logger, String 
message, long minInterval, TimeUnit unit) {
+        NoSpamLogger wrapped = getLogger(logger, minInterval, unit);
+        return wrapped.getStatement(message);
+    }
+
+    private final Logger wrapped;
+    private final long minIntervalNanos;
+    private final NonBlockingHashMap<String, NoSpamLogStatement> lastMessage = 
new NonBlockingHashMap<>();
+
+    private NoSpamLogger(Logger wrapped, long minInterval, TimeUnit timeUnit)
+    {
+        this.wrapped = wrapped;
+        minIntervalNanos = timeUnit.toNanos(minInterval);
+    }
+
+    public void info(long nowNanos, String s, Object... objects)
+    {
+        log( Level.INFO, s, nowNanos, objects);
+    }
+
+    public void info(String s, Object... objects)
+    {
+        info(CLOCK.nanoTime(), s, objects);
+    }
+
+    public void warn(long nowNanos, String s, Object... objects)
+    {
+        log( Level.WARN, s, nowNanos, objects);
+    }
+
+    public void warn(String s, Object... objects)
+    {
+        warn(CLOCK.nanoTime(), s, objects);
+    }
+
+    public void error(long nowNanos, String s, Object... objects)
+    {
+        log( Level.ERROR, s, nowNanos, objects);
+    }
+
+    public void error(String s, Object... objects)
+    {
+        error(CLOCK.nanoTime(), s, objects);
+    }
+
+    public void log(Level l, String s, long nowNanos, Object... objects) {
+        getStatement(s, minIntervalNanos).log(l, nowNanos, objects);
+    }
+
+    public NoSpamLogStatement getStatement(String s)
+    {
+        return getStatement(s, minIntervalNanos);
+    }
+
+    public NoSpamLogStatement getStatement(String s, long minInterval, 
TimeUnit unit) {
+        return getStatement(s, unit.toNanos(minInterval));
+    }
+
+    public NoSpamLogStatement getStatement(String s, long minIntervalNanos)
+    {
+        NoSpamLogStatement statement = lastMessage.get(s);
+        if (statement == null)
+        {
+            statement = new NoSpamLogStatement(s, minIntervalNanos);
+            NoSpamLogStatement temp = lastMessage.putIfAbsent(s, statement);
+            if (temp != null)
+                statement = temp;
+        }
+        return statement;
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/5bffaf85/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java 
b/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java
new file mode 100644
index 0000000..3ad7ab8
--- /dev/null
+++ b/test/unit/org/apache/cassandra/utils/NoSpamLoggerTest.java
@@ -0,0 +1,174 @@
+/*
+* 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.cassandra.utils;
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.cassandra.utils.NoSpamLogger.Level;
+import org.apache.cassandra.utils.NoSpamLogger.NoSpamLogStatement;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.helpers.SubstituteLogger;
+
+
+public class NoSpamLoggerTest
+{
+    Map<Level, List<Pair<String, Object[]>>> logged = new HashMap<>();
+
+   Logger mock = new SubstituteLogger(null)
+   {
+
+       @Override
+       public void info(String statement, Object... args)
+       {
+           logged.get(Level.INFO).add(Pair.create(statement, args));
+       }
+
+       @Override
+       public void warn(String statement, Object... args)
+       {
+           logged.get(Level.WARN).add(Pair.create(statement, args));
+       }
+
+       @Override
+       public void error(String statement, Object... args)
+       {
+           logged.get(Level.ERROR).add(Pair.create(statement, args));
+       }
+
+       @Override
+       public int hashCode()
+       {
+           return 42;//It's a valid hash code
+       }
+
+       @Override
+       public boolean equals(Object o)
+       {
+           return this == o;
+       }
+   };
+
+
+   static long now;
+
+   @BeforeClass
+   public static void setUpClass() throws Exception
+   {
+       NoSpamLogger.CLOCK = new NoSpamLogger.Clock()
+       {
+        @Override
+        public long nanoTime()
+        {
+            return now;
+        }
+       };
+   }
+
+   @Before
+   public void setUp() throws Exception
+   {
+       logged.put(Level.INFO, new ArrayList<Pair<String, Object[]>>());
+       logged.put(Level.WARN, new ArrayList<Pair<String, Object[]>>());
+       logged.put(Level.ERROR, new ArrayList<Pair<String, Object[]>>());
+   }
+
+   @Test
+   public void testNoSpamLogger() throws Exception
+   {
+       testLevel(Level.INFO);
+       testLevel(Level.WARN);
+       testLevel(Level.ERROR);
+   }
+
+   private void testLevel(Level l) throws Exception
+   {
+       setUp();
+       now = 5;
+       NoSpamLogger.clearWrappedLoggersForTest();
+
+       NoSpamLogger.log( mock, l, 5,  TimeUnit.NANOSECONDS, "swizzle{}", "a");
+
+       assertEquals(1, logged.get(l).size());
+
+       NoSpamLogger.log( mock, l, 5,  TimeUnit.NANOSECONDS, "swizzle{}", "a");
+
+       assertEquals(1, logged.get(l).size());
+
+       now += 5;
+
+       NoSpamLogger.log( mock, l, 5,  TimeUnit.NANOSECONDS, "swizzle{}", "a");
+
+       assertEquals(2, logged.get(l).size());
+   }
+
+   private void assertLoggedSizes(int info, int warn, int error)
+   {
+       assertEquals(info, logged.get(Level.INFO).size());
+       assertEquals(warn, logged.get(Level.WARN).size());
+       assertEquals(error, logged.get(Level.ERROR).size());
+   }
+
+   @Test
+   public void testNoSpamLoggerDirect() throws Exception
+   {
+       now = 5;
+       NoSpamLogger logger = NoSpamLogger.getLogger( mock, 5, 
TimeUnit.NANOSECONDS);
+
+       logger.info("swizzle{}", "a");
+       logger.info("swizzle{}", "a");
+       logger.warn("swizzle{}", "a");
+       logger.error("swizzle{}", "a");
+
+       assertLoggedSizes(1, 0, 0);
+
+       NoSpamLogStatement statement = logger.getStatement("swizzle2{}", 10, 
TimeUnit.NANOSECONDS);
+       statement.warn("a");
+       //now is 5 so it won't log
+       assertLoggedSizes(1, 0, 0);
+
+       now = 10;
+       statement.warn("a");
+       assertLoggedSizes(1, 1, 0);
+
+   }
+
+   @Test
+   public void testNoSpamLoggerStatementDirect() throws Exception
+   {
+       NoSpamLogger.NoSpamLogStatement statement = NoSpamLogger.getStatement( 
mock, "swizzle{}", 5, TimeUnit.NANOSECONDS);
+
+       now = 5;
+
+       statement.info("swizzle{}", "a");
+       statement.info("swizzle{}", "a");
+       statement.warn("swizzle{}", "a");
+       statement.error("swizzle{}", "a");
+
+       assertLoggedSizes(1, 0, 0);
+   }
+}

Reply via email to