You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ag...@apache.org on 2018/10/18 16:10:39 UTC

ignite git commit: IGNITE-8570 Improved GridToStringLogger - Fixes #4786.

Repository: ignite
Updated Branches:
  refs/heads/master 5eb871e19 -> a3b624d9f


IGNITE-8570 Improved GridToStringLogger - Fixes #4786.

Signed-off-by: Alexey Goncharuk <al...@gmail.com>


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

Branch: refs/heads/master
Commit: a3b624d9f34012eab1711dac14dc71af3c5bd9c4
Parents: 5eb871e
Author: pereslegin-pa <xx...@gmail.com>
Authored: Thu Oct 18 19:08:44 2018 +0300
Committer: Alexey Goncharuk <al...@gmail.com>
Committed: Thu Oct 18 19:08:44 2018 +0300

----------------------------------------------------------------------
 .../ignite/testframework/GridStringLogger.java  |   3 +
 .../testframework/ListeningTestLogger.java      | 205 +++++++++
 .../ignite/testframework/LogListener.java       | 427 ++++++++++++++++++
 .../test/ListeningTestLoggerTest.java           | 428 +++++++++++++++++++
 .../ignite/testsuites/IgniteBasicTestSuite.java |   3 +
 5 files changed, 1066 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/a3b624d9/modules/core/src/test/java/org/apache/ignite/testframework/GridStringLogger.java
----------------------------------------------------------------------
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/GridStringLogger.java b/modules/core/src/test/java/org/apache/ignite/testframework/GridStringLogger.java
index 9056dd6..0b09d3e 100644
--- a/modules/core/src/test/java/org/apache/ignite/testframework/GridStringLogger.java
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/GridStringLogger.java
@@ -26,7 +26,10 @@ import org.jetbrains.annotations.Nullable;
 
 /**
  * Logger which logs to string buffer.
+ *
+ * @deprecated Use {@link ListeningTestLogger} instead.
  */
+@Deprecated
 public class GridStringLogger implements IgniteLogger {
     /** Initial string builder capacity in bytes */
     private static final int INITIAL = 1024 * 33;

http://git-wip-us.apache.org/repos/asf/ignite/blob/a3b624d9/modules/core/src/test/java/org/apache/ignite/testframework/ListeningTestLogger.java
----------------------------------------------------------------------
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/ListeningTestLogger.java b/modules/core/src/test/java/org/apache/ignite/testframework/ListeningTestLogger.java
new file mode 100644
index 0000000..1b05f4c
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/ListeningTestLogger.java
@@ -0,0 +1,205 @@
+/*
+ * 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.ignite.testframework;
+
+import java.util.Collection;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.function.Consumer;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.internal.util.typedef.X;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Implementation of {@link org.apache.ignite.IgniteLogger} that performs any actions when certain message is logged.
+ * It can be useful in tests to ensure that a specific message was (or was not) printed to the log.
+ */
+public class ListeningTestLogger implements IgniteLogger {
+    /**
+     * If set to {@code true}, enables debug and trace log messages processing.
+     */
+    private final boolean dbg;
+
+    /**
+     * Logger to echo all messages, limited by {@code dbg} flag.
+     */
+    private final IgniteLogger echo;
+
+    /**
+     * Registered log messages listeners.
+     */
+    private final Collection<Consumer<String>> lsnrs = new CopyOnWriteArraySet<>();
+
+    /**
+     * Default constructor.
+     */
+    public ListeningTestLogger() {
+        this(false);
+    }
+
+    /**
+     * @param dbg If set to {@code true}, enables debug and trace log messages processing.
+     */
+    public ListeningTestLogger(boolean dbg) {
+        this(dbg, null);
+    }
+
+    /**
+     * @param dbg If set to {@code true}, enables debug and trace log messages processing.
+     * @param echo Logger to echo all messages, limited by {@code dbg} flag.
+     */
+    public ListeningTestLogger(boolean dbg, @Nullable IgniteLogger echo) {
+        this.dbg = dbg;
+        this.echo = echo;
+    }
+
+    /**
+     * Registers message listener.
+     *
+     * @param lsnr Message listener.
+     */
+    public void registerListener(@NotNull LogListener lsnr) {
+        lsnr.reset();
+
+        lsnrs.add(lsnr);
+    }
+
+    /**
+     * Registers message listener.
+     * <p>
+     * NOTE listener is executed in the thread causing the logging, so it is not recommended to throw any exceptions
+     * from it. Use {@link LogListener} to create message predicates with assertions.
+     *
+     * @param lsnr Message listener.
+     * @see LogListener
+     */
+    public void registerListener(@NotNull Consumer<String> lsnr) {
+        lsnrs.add(lsnr);
+    }
+
+    /**
+     * Unregisters message listener.
+     *
+     * @param lsnr Message listener.
+     */
+    public void unregisterListener(@NotNull Consumer<String> lsnr) {
+        lsnrs.remove(lsnr);
+    }
+
+    /**
+     * Clears all listeners.
+     */
+    public void clearListeners() {
+        lsnrs.clear();
+    }
+
+    /** {@inheritDoc} */
+    @Override public ListeningTestLogger getLogger(Object ctgr) {
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void trace(String msg) {
+        if (!dbg)
+            return;
+
+        if (echo != null)
+            echo.trace(msg);
+
+        applyListeners(msg);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void debug(String msg) {
+        if (!dbg)
+            return;
+
+        if (echo != null)
+            echo.debug(msg);
+
+        applyListeners(msg);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void info(String msg) {
+        if (echo != null)
+            echo.info(msg);
+
+        applyListeners(msg);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void warning(String msg, @Nullable Throwable t) {
+        if (echo != null)
+            echo.warning(msg, t);
+
+        applyListeners(msg);
+
+        if (t != null)
+            applyListeners(X.getFullStackTrace(t));
+    }
+
+    /** {@inheritDoc} */
+    @Override public void error(String msg, @Nullable Throwable t) {
+        if (echo != null)
+            echo.error(msg, t);
+
+        applyListeners(msg);
+
+        if (t != null)
+            applyListeners(X.getFullStackTrace(t));
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isTraceEnabled() {
+        return dbg;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isDebugEnabled() {
+        return dbg;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isInfoEnabled() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isQuiet() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String fileName() {
+        return null;
+    }
+
+    /**
+     * Applies listeners whose pattern is found in the message.
+     *
+     * @param msg Message to check.
+     */
+    private void applyListeners(String msg) {
+        if (msg == null)
+            return;
+
+        for (Consumer<String> lsnr : lsnrs)
+            lsnr.accept(msg);
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/a3b624d9/modules/core/src/test/java/org/apache/ignite/testframework/LogListener.java
----------------------------------------------------------------------
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/LogListener.java b/modules/core/src/test/java/org/apache/ignite/testframework/LogListener.java
new file mode 100644
index 0000000..d349f06
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/LogListener.java
@@ -0,0 +1,427 @@
+/*
+ * 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.ignite.testframework;
+
+import java.time.temporal.ValueRange;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The basic listener for custom log contents checking in {@link ListeningTestLogger}.<br><br>
+ *
+ * Supports {@link #matches(String) substring}, {@link #matches(Pattern) regular expression} or
+ * {@link #matches(Predicate) predicate} listeners and the following optional modifiers:
+ * <ul>
+ *  <li>{@link Builder#times times()} sets the exact number of occurrences</li>
+ *  <li>{@link Builder#atLeast atLeast()} sets the minimum number of occurrences</li>
+ *  <li>{@link Builder#atMost atMost()} sets the maximum number of occurrences</li>
+ * </ul>
+ * {@link Builder#atLeast atLeast()} and {@link Builder#atMost atMost()} can be used together.<br><br>
+ *
+ * If the expected number of occurrences is not specified for the listener,
+ * then at least one occurence is expected by default. In other words:<pre>
+ *
+ * {@code LogListener.matches(msg).build();}
+ *
+ * is equivalent to
+ *
+ * {@code LogListener.matches(msg).atLeast(1).build();}
+ * </pre>
+ *
+ * If only the expected maximum number of occurrences is specified, then
+ * the minimum number of entries for successful validation is zero. In other words:<pre>
+ *
+ * {@code LogListener.matches(msg).atMost(10).build();}
+ *
+ * is equivalent to
+ *
+ * {@code LogListener.matches(msg).atLeast(0).atMost(10).build();}
+ * </pre>
+ */
+public abstract class LogListener implements Consumer<String> {
+    /**
+     * Checks that all conditions are met.
+     *
+     * @throws AssertionError If some condition failed.
+     */
+    public abstract void check() throws AssertionError;
+
+    /**
+     * Reset listener state.
+     */
+    abstract void reset();
+
+    /**
+     * Creates new listener builder.
+     *
+     * @param substr Substring to search for in a log message.
+     * @return Log message listener builder.
+     */
+    public static Builder matches(String substr) {
+        return new Builder().andMatches(substr);
+    }
+
+    /**
+     * Creates new listener builder.
+     *
+     * @param regexp Regular expression to search for in a log message.
+     * @return Log message listener builder.
+     */
+    public static Builder matches(Pattern regexp) {
+        return new Builder().andMatches(regexp);
+    }
+
+    /**
+     * Creates new listener builder.
+     *
+     * @param pred Log message predicate.
+     * @return Log message listener builder.
+     */
+    public static Builder matches(Predicate<String> pred) {
+        return new Builder().andMatches(pred);
+    }
+
+    /**
+     * Log listener builder.
+     */
+    public static class Builder {
+        /** */
+        private final CompositeMessageListener lsnr = new CompositeMessageListener();
+
+        /** */
+        private Node prev;
+
+        /**
+         * Add new substring predicate.
+         *
+         * @param substr Substring.
+         * @return current builder instance.
+         */
+        public Builder andMatches(String substr) {
+            addLast(new Node(substr, msg -> {
+                if (substr.isEmpty())
+                    return msg.isEmpty() ? 1 : 0;
+
+                int cnt = 0;
+
+                for (int idx = 0; (idx = msg.indexOf(substr, idx)) != -1; idx++)
+                    ++cnt;
+
+                return cnt;
+            }));
+
+            return this;
+        }
+
+        /**
+         * Add new regular expression predicate.
+         *
+         * @param regexp Regular expression.
+         * @return current builder instance.
+         */
+        public Builder andMatches(Pattern regexp) {
+            addLast(new Node(regexp.toString(), msg -> {
+                int cnt = 0;
+
+                Matcher matcher = regexp.matcher(msg);
+
+                while (matcher.find())
+                    ++cnt;
+
+                return cnt;
+            }));
+
+            return this;
+        }
+
+        /**
+         * Add new log message predicate.
+         *
+         * @param pred Log message predicate.
+         * @return current builder instance.
+         */
+        public Builder andMatches(Predicate<String> pred) {
+            addLast(new Node(null, msg -> pred.test(msg) ? 1 : 0));
+
+            return this;
+        }
+
+        /**
+         * Set expected number of matches.<br>
+         * Each log message may contain several matches that will be counted,
+         * except {@code Predicate} which can have only one match for message.
+         *
+         * @param n Expected number of matches.
+         * @return current builder instance.
+         */
+        public Builder times(int n) {
+            if (prev != null)
+                prev.cnt = n;
+
+            return this;
+        }
+
+        /**
+         * Set expected minimum number of matches.<br>
+         * Each log message may contain several matches that will be counted,
+         * except {@code Predicate} which can have only one match for message.
+         *
+         * @param n Expected number of matches.
+         * @return current builder instance.
+         */
+        public Builder atLeast(int n) {
+            if (prev != null) {
+                prev.min = n;
+
+                prev.cnt = null;
+            }
+
+            return this;
+        }
+
+        /**
+         * Set expected maximum number of matches.<br>
+         * Each log message may contain several matches that will be counted,
+         * except {@code Predicate} which can have only one match for message.
+         *
+         * @param n Expected number of matches.
+         * @return current builder instance.
+         */
+        public Builder atMost(int n) {
+            if (prev != null) {
+                prev.max = n;
+
+                prev.cnt = null;
+            }
+
+            return this;
+        }
+
+        /**
+         * Set custom message for assertion error.
+         *
+         * @param msg Custom message.
+         * @return current builder instance.
+         */
+        public Builder orError(String msg) {
+            if (prev != null)
+                prev.msg = msg;
+
+            return this;
+        }
+
+        /**
+         * Constructs message listener.
+         *
+         * @return Log message listener.
+         */
+        public LogListener build() {
+            addLast(null);
+
+            return lsnr.lsnrs.size() == 1 ? lsnr.lsnrs.get(0) : lsnr;
+        }
+
+        /**
+         * @param node Log listener attributes.
+         */
+        private void addLast(Node node) {
+            if (prev != null)
+                lsnr.add(prev.listener());
+
+            prev = node;
+        }
+
+        /** */
+        private Builder() {}
+
+        /**
+         * Mutable attributes for log listener.
+         */
+        static final class Node {
+            /** */
+            final String subj;
+
+            /** */
+            final Function<String, Integer> func;
+
+            /** */
+            String msg;
+
+            /** */
+            Integer min;
+
+            /** */
+            Integer max;
+
+            /** */
+            Integer cnt;
+
+            /** */
+            Node(String subj, Function<String, Integer> func) {
+                this.subj = subj;
+                this.func = func;
+            }
+
+            /** */
+            LogMessageListener listener() {
+                ValueRange range;
+
+                if (cnt != null)
+                    range = ValueRange.of(cnt, cnt);
+                else if (min == null && max == null)
+                    range = ValueRange.of(1, Integer.MAX_VALUE);
+                else
+                    range = ValueRange.of(min == null ? 0 : min, max == null ? Integer.MAX_VALUE : max);
+
+                return new LogMessageListener(func, range, subj, msg);
+            }
+        }
+    }
+
+    /** */
+    private static class LogMessageListener extends LogListener {
+        /** */
+        private final Function<String, Integer> func;
+
+        /** */
+        private final AtomicReference<Throwable> err = new AtomicReference<>();
+
+        /** */
+        private final AtomicInteger matches = new AtomicInteger();
+
+        /** */
+        private final ValueRange exp;
+
+        /** */
+        private final String subj;
+
+        /** */
+        private final String errMsg;
+
+        /**
+         * @param subj Search subject.
+         * @param exp Expected occurrences.
+         * @param func Function of counting matches in the message.
+         * @param errMsg Custom error message.
+         */
+        private LogMessageListener(
+            @NotNull Function<String, Integer> func,
+            @NotNull ValueRange exp,
+            @Nullable String subj,
+            @Nullable String errMsg
+        ) {
+            this.func = func;
+            this.exp = exp;
+            this.subj = subj == null ? func.toString() : subj;
+            this.errMsg = errMsg;
+        }
+
+        /** {@inheritDoc} */
+        @Override public void accept(String msg) {
+            if (err.get() != null)
+                return;
+
+            try {
+                int cnt = func.apply(msg);
+
+                if (cnt > 0)
+                    matches.addAndGet(cnt);
+            } catch (Throwable t) {
+                err.compareAndSet(null, t);
+
+                if (t instanceof VirtualMachineError)
+                    throw t;
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override public void check() {
+            errCheck();
+
+            int matchesCnt = matches.get();
+
+            if (!exp.isValidIntValue(matchesCnt)) {
+                String err =  errMsg != null ? errMsg :
+                    "\"" + subj + "\" matches " + matchesCnt + " times, expected: " +
+                        (exp.getMaximum() == exp.getMinimum() ? exp.getMinimum() : exp) + ".";
+
+                throw new AssertionError(err);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override void reset() {
+            matches.set(0);
+        }
+
+        /**
+         * Check that there were no runtime errors.
+         */
+        private void errCheck() {
+            Throwable t = err.get();
+
+            if (t instanceof Error)
+                throw (Error) t;
+
+            if (t instanceof RuntimeException)
+                throw (RuntimeException) t;
+
+            assert t == null : t;
+        }
+    }
+
+    /** */
+    private static class CompositeMessageListener extends LogListener {
+        /** */
+        private final List<LogMessageListener> lsnrs = new ArrayList<>();
+
+        /** {@inheritDoc} */
+        @Override public void check() {
+            for (LogMessageListener lsnr : lsnrs)
+                lsnr.check();
+        }
+
+        /** {@inheritDoc} */
+        @Override void reset() {
+            for (LogMessageListener lsnr : lsnrs)
+                lsnr.reset();
+        }
+
+        /** {@inheritDoc} */
+        @Override public void accept(String msg) {
+            for (LogMessageListener lsnr : lsnrs)
+                lsnr.accept(msg);
+        }
+
+        /**
+         * @param lsnr Listener.
+         */
+        private void add(LogMessageListener lsnr) {
+            lsnrs.add(lsnr);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/a3b624d9/modules/core/src/test/java/org/apache/ignite/testframework/test/ListeningTestLoggerTest.java
----------------------------------------------------------------------
diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/test/ListeningTestLoggerTest.java b/modules/core/src/test/java/org/apache/ignite/testframework/test/ListeningTestLoggerTest.java
new file mode 100644
index 0000000..a888017
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/testframework/test/ListeningTestLoggerTest.java
@@ -0,0 +1,428 @@
+/*
+ * 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.ignite.testframework.test;
+
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+import org.apache.ignite.IgniteLogger;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.internal.IgniteVersionUtils;
+import org.apache.ignite.logger.NullLogger;
+import org.apache.ignite.testframework.GridTestUtils;
+import org.apache.ignite.testframework.ListeningTestLogger;
+import org.apache.ignite.testframework.LogListener;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+
+import static org.apache.ignite.testframework.GridTestUtils.assertThrows;
+import static org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
+
+/**
+ * Test.
+ */
+@SuppressWarnings("ThrowableNotThrown")
+public class ListeningTestLoggerTest extends GridCommonAbstractTest {
+    /** */
+    private final ListeningTestLogger log = new ListeningTestLogger(false, super.log);
+
+    /** {@inheritDoc} */
+    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
+        IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName);
+
+        cfg.setGridLogger(log);
+
+        return cfg;
+    }
+
+    /**
+     * Basic example of using listening logger - checks that all running instances of Ignite print product version.
+     *
+     * @throws Exception If failed.
+     */
+    public void testIgniteVersionLogging() throws Exception {
+        int gridCnt = 4;
+
+        LogListener lsnr = LogListener.matches(IgniteVersionUtils.VER_STR).atLeast(gridCnt).build();
+
+        log.registerListener(lsnr);
+
+        try {
+            startGridsMultiThreaded(gridCnt);
+
+            lsnr.check();
+        } finally {
+            stopAllGrids();
+        }
+    }
+
+    /**
+     * Checks that re-register works fine.
+     */
+    public void testUnregister() {
+        String msg = "catch me";
+
+        LogListener lsnr1 = LogListener.matches(msg).times(1).build();
+        LogListener lsnr2 = LogListener.matches(msg).times(2).build();
+
+        log.registerListener(lsnr1);
+        log.registerListener(lsnr2);
+
+        log.info(msg);
+
+        log.unregisterListener(lsnr1);
+
+        log.info(msg);
+
+        lsnr1.check();
+        lsnr2.check();
+
+        // Repeat these steps to ensure that the state is cleared during registration.
+        log.registerListener(lsnr1);
+        log.registerListener(lsnr2);
+
+        log.info(msg);
+
+        log.unregisterListener(lsnr1);
+
+        log.info(msg);
+
+        lsnr1.check();
+        lsnr2.check();
+    }
+
+    /**
+     * Ensures that listener will be re-registered only once.
+     */
+    public void testRegister() {
+        AtomicInteger cntr = new AtomicInteger();
+
+        LogListener lsnr3 = LogListener.matches(m -> cntr.incrementAndGet() > 0).build();
+
+        log.registerListener(lsnr3);
+        log.registerListener(lsnr3);
+
+        log.info("1");
+
+        assertEquals(1, cntr.get());
+    }
+
+    /**
+     * Checks basic API.
+     */
+    public void testBasicApi() {
+        String errMsg = "Word started with \"a\" not found.";
+
+        LogListener lsnr = LogListener.matches(Pattern.compile("a[a-z]+")).orError(errMsg)
+            .andMatches("Exception message.").andMatches(".java:").build();
+
+        log.registerListener(lsnr);
+
+        log.info("Something new.");
+
+        assertThrows(log(), () -> {
+            lsnr.check();
+
+            return null;
+        }, AssertionError.class, errMsg);
+
+        log.error("There was an error.", new RuntimeException("Exception message."));
+
+        lsnr.check();
+    }
+
+    /**
+     * Checks blank lines matching.
+     */
+    public void testEmptyLine() {
+        LogListener emptyLineLsnr = LogListener.matches("").build();
+
+        log.registerListener(emptyLineLsnr);
+
+        log.info("");
+
+        emptyLineLsnr.check();
+    }
+
+    /** */
+    public void testPredicateExceptions() {
+        LogListener lsnr = LogListener.matches(msg -> {
+            assertFalse(msg.contains("Target"));
+
+            return true;
+        }).build();
+
+        log.registerListener(lsnr);
+
+        log.info("Ignored message.");
+        log.info("Target message.");
+
+        assertThrowsWithCause(lsnr::check, AssertionError.class);
+
+        // Check custom exception.
+        LogListener lsnr2 = LogListener.matches(msg -> {
+            throw new IllegalStateException("Illegal state");
+        }).orError("ignored blah-blah").build();
+
+        log.registerListener(lsnr2);
+
+        log.info("1");
+        log.info("2");
+
+        assertThrowsWithCause(lsnr2::check, IllegalStateException.class);
+    }
+
+    /**
+     * Validates listener range definition.
+     */
+    public void testRange() {
+        String msg = "range";
+
+        LogListener lsnr2 = LogListener.matches(msg).times(2).build();
+        LogListener lsnr2_3 = LogListener.matches(msg).atLeast(2).atMost(3).build();
+
+        log.registerListener(lsnr2);
+        log.registerListener(lsnr2_3);
+
+        log.info(msg);
+        log.info(msg);
+
+        lsnr2.check();
+        lsnr2_3.check();
+
+        log.info(msg);
+
+        assertThrowsWithCause(lsnr2::check, AssertionError.class);
+
+        lsnr2_3.check();
+
+        log.info(msg);
+
+        assertThrowsWithCause(lsnr2_3::check, AssertionError.class);
+    }
+
+    /**
+     * Checks that substring was not found in the log messages.
+     */
+    public void testNotPresent() {
+        String msg = "vacuum";
+
+        LogListener notPresent = LogListener.matches(msg).times(0).build();
+
+        log.registerListener(notPresent);
+
+        log.info("1");
+
+        notPresent.check();
+
+        log.info(msg);
+
+        assertThrowsWithCause(notPresent::check, AssertionError.class);
+    }
+
+    /**
+     * Checks that the substring is found at least twice.
+     */
+    public void testAtLeast() {
+        String msg = "at least";
+
+        LogListener atLeast2 = LogListener.matches(msg).atLeast(2).build();
+
+        log.registerListener(atLeast2);
+
+        log.info(msg);
+
+        assertThrowsWithCause(atLeast2::check, AssertionError.class);
+
+        log.info(msg);
+
+        atLeast2.check();
+    }
+
+    /**
+     * Checks that the substring is found no more than twice.
+     */
+    public void testAtMost() {
+        String msg = "at most";
+
+        LogListener atMost2 = LogListener.matches(msg).atMost(2).build();
+
+        log.registerListener(atMost2);
+
+        atMost2.check();
+
+        log.info(msg);
+        log.info(msg);
+
+        atMost2.check();
+
+        log.info(msg);
+
+        assertThrowsWithCause(atMost2::check, AssertionError.class);
+    }
+
+    /**
+     * Checks that only last value is taken into account.
+     */
+    public void testMultiRange() {
+        String msg = "multi range";
+
+        LogListener atMost3 = LogListener.matches(msg).times(1).times(2).atMost(3).build();
+
+        log.registerListener(atMost3);
+
+        for (int i = 0; i < 6; i++) {
+            if (i < 4)
+                atMost3.check();
+            else
+                assertThrowsWithCause(atMost3::check, AssertionError.class);
+
+            log.info(msg);
+        }
+
+        LogListener lsnr4 = LogListener.matches(msg).atLeast(2).atMost(3).times(4).build();
+
+        log.registerListener(lsnr4);
+
+        for (int i = 1; i < 6; i++) {
+            log.info(msg);
+
+            if (i == 4)
+                lsnr4.check();
+            else
+                assertThrowsWithCause(lsnr4::check, AssertionError.class);
+        }
+    }
+
+    /**
+     * Checks that matches are counted for each message.
+     */
+    public void testMatchesPerMessage() {
+        LogListener lsnr = LogListener.matches("aa").times(4).build();
+
+        log.registerListener(lsnr);
+
+        log.info("aabaab");
+        log.info("abaaab");
+
+        lsnr.check();
+
+        LogListener newLineLsnr = LogListener.matches("\n").times(5).build();
+
+        log.registerListener(newLineLsnr);
+
+        log.info("\n1\n2\n\n3\n");
+
+        newLineLsnr.check();
+
+        LogListener regexpLsnr = LogListener.matches(Pattern.compile("(?i)hi|hello")).times(3).build();
+
+        log.registerListener(regexpLsnr);
+
+        log.info("Hi! Hello!");
+        log.info("Hi folks");
+
+        regexpLsnr.check();
+    }
+
+    /**
+     * Check thread safety.
+     *
+     * @throws Exception If failed.
+     */
+    public void testMultithreaded() throws Exception {
+        int iterCnt = 50_000;
+        int threadCnt = 6;
+        int total = threadCnt * iterCnt;
+        int rndNum = ThreadLocalRandom.current().nextInt(iterCnt);
+
+        LogListener lsnr = LogListener.matches("abba").times(total)
+            .andMatches(Pattern.compile("(?i)abba")).times(total * 2)
+            .andMatches("ab").times(total)
+            .andMatches("ba").times(total)
+            .build();
+
+        LogListener mtLsnr = LogListener.matches("abba").build();
+
+        log.registerListener(lsnr);
+
+        GridTestUtils.runMultiThreaded(() -> {
+            for (int i = 0; i < iterCnt; i++) {
+                if (rndNum == i)
+                    log.registerListener(mtLsnr);
+
+                log.info("It is the abba(ABBA) message.");
+            }
+        }, threadCnt, "test-listening-log");
+
+        lsnr.check();
+        mtLsnr.check();
+    }
+
+    /**
+     * Check "echo" logger.
+     */
+    public void testEchoLogger() {
+        IgniteLogger echo = new StringLogger();
+
+        ListeningTestLogger log = new ListeningTestLogger(true, echo);
+
+        log.error("1");
+        log.warning("2");
+        log.info("3");
+        log.debug("4");
+        log.trace("5");
+
+        assertEquals("12345", echo.toString());
+    }
+
+    /** */
+    private static class StringLogger extends NullLogger {
+        /** */
+        private final StringBuilder buf = new StringBuilder();
+
+        /** {@inheritDoc} */
+        @Override public void trace(String msg) {
+            buf.append(msg);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void debug(String msg) {
+            buf.append(msg);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void info(String msg) {
+            buf.append(msg);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void warning(String msg, Throwable t) {
+            buf.append(msg);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void error(String msg, Throwable t) {
+            buf.append(msg);
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return buf.toString();
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/a3b624d9/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
----------------------------------------------------------------------
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
index ac2bed3..32cd36e 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteBasicTestSuite.java
@@ -87,6 +87,7 @@ import org.apache.ignite.startup.properties.NotStringSystemPropertyTest;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.GridAbstractTest;
 import org.apache.ignite.testframework.test.ConfigVariationsTestSuiteBuilderTest;
+import org.apache.ignite.testframework.test.ListeningTestLoggerTest;
 import org.apache.ignite.testframework.test.ParametersTest;
 import org.apache.ignite.testframework.test.VariationsIteratorTest;
 import org.apache.ignite.util.AttributeNodeFilterSelfTest;
@@ -217,6 +218,8 @@ public class IgniteBasicTestSuite extends TestSuite {
 
         suite.addTestSuite(CacheRebalanceConfigValidationTest.class);
 
+        suite.addTestSuite(ListeningTestLoggerTest.class);
+
         return suite;
     }
 }