You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by am...@apache.org on 2021/04/14 10:06:31 UTC

[ignite-3] branch main updated: IGNITE-14501: Ignite 3: Fix toString implementations. (#88)

This is an automated email from the ASF dual-hosted git repository.

amashenkov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 28f0927  IGNITE-14501: Ignite 3: Fix toString implementations. (#88)
28f0927 is described below

commit 28f0927e7dd242fdb2c9c341dbac6e3939c65fe1
Author: Andrew V. Mashenkov <AM...@users.noreply.github.com>
AuthorDate: Wed Apr 14 13:06:17 2021 +0300

    IGNITE-14501: Ignite 3: Fix toString implementations. (#88)
---
 .../presto/bytecode/AnnotationDefinition.java      |    4 +-
 modules/cli/pom.xml                                |    6 +
 .../ignite/cli/builtins/module/ModuleRegistry.java |   23 +-
 .../builtins/module/StandardModuleDefinition.java  |    8 +-
 .../configuration/validation/ValidationIssue.java  |    8 +-
 .../internal/tostring/CircularStringBuilder.java   |  277 +++
 .../ignite/internal/tostring/ClassDescriptor.java  |   86 +
 .../ignite/internal/tostring/FieldDescriptor.java  |  142 ++
 .../internal/tostring/IgniteToStringBuilder.java   | 2110 ++++++++++++++++++++
 .../internal/tostring/IgniteToStringExclude.java}  |   33 +-
 .../internal/tostring/IgniteToStringInclude.java   |   45 +
 .../internal/tostring/IgniteToStringOrder.java}    |   42 +-
 .../org/apache/ignite/internal/tostring/S.java}    |   31 +-
 .../ignite/internal/tostring/SBLimitedLength.java  |  287 +++
 .../tostring/SensitiveDataLoggingPolicy.java}      |   34 +-
 .../ignite/internal/tostring/package-info.java}    |   25 +-
 .../apache/ignite/internal/util/IgniteUtils.java   |   54 +
 .../apache/ignite/lang/IgniteStringBuilder.java    |  504 +++++
 .../apache/ignite/lang/IgniteSystemProperties.java |  307 +++
 .../internal/testframework/IgniteAbstractTest.java |   54 +
 .../testframework/SystemPropertiesExtension.java   |  198 ++
 .../testframework/SystemPropertiesList.java}       |   32 +-
 .../internal/testframework/WithSystemProperty.java |  114 ++
 .../internal/testframework/package-info.java}      |   25 +-
 .../tostring/CircularStringBuilderSelfTest.java    |   72 +
 .../tostring/IgniteToStringBuilderSelfTest.java    |  994 +++++++++
 .../tostring/SensitiveDataToStringTest.java        |  180 ++
 modules/network/pom.xml                            |   13 +-
 .../org/apache/ignite/network/TestMessage.java     |   17 +-
 .../org/apache/ignite/network/ClusterNode.java     |    7 +-
 .../java/org/apache/ignite/raft/client/Peer.java   |    6 +-
 .../internal/schema/AbstractSchemaObject.java      |    8 +-
 .../org/apache/ignite/internal/schema/Bitmask.java |    7 +
 .../org/apache/ignite/internal/schema/Column.java  |    4 +-
 .../apache/ignite/internal/schema/ColumnImpl.java  |   10 +-
 .../org/apache/ignite/internal/schema/Columns.java |    6 +
 .../ignite/internal/schema/HashIndexImpl.java      |   11 +-
 .../ignite/internal/schema/IndexColumnImpl.java    |    5 +-
 .../apache/ignite/internal/schema/NativeType.java  |    9 +-
 .../ignite/internal/schema/NativeTypeSpec.java     |    6 +-
 .../ignite/internal/schema/PartialIndexImpl.java   |   14 +-
 .../ignite/internal/schema/PrimaryIndexImpl.java   |   17 +
 .../ignite/internal/schema/SchemaDescriptor.java   |    6 +
 .../ignite/internal/schema/SchemaTableImpl.java    |    9 +-
 .../internal/schema/SortedIndexColumnImpl.java     |    6 +-
 .../ignite/internal/schema/SortedIndexImpl.java    |   15 +-
 46 files changed, 5640 insertions(+), 231 deletions(-)

diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AnnotationDefinition.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AnnotationDefinition.java
index 6a1d41d..4d35fc5 100644
--- a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AnnotationDefinition.java
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AnnotationDefinition.java
@@ -13,6 +13,7 @@
  */
 package com.facebook.presto.bytecode;
 
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -126,8 +127,7 @@ public class AnnotationDefinition {
     }
 
     public Map<String, Object> getValues() {
-        // todo we need an unmodifiable view
-        return values;
+        return Collections.unmodifiableMap(values);
     }
 
     @SuppressWarnings("OverlyStrongTypeCast")
diff --git a/modules/cli/pom.xml b/modules/cli/pom.xml
index fed7c40..1506e54 100644
--- a/modules/cli/pom.xml
+++ b/modules/cli/pom.xml
@@ -41,6 +41,12 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <!-- 3-rd party dependencies. -->
         <dependency>
             <groupId>ch.qos.logback</groupId>
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleRegistry.java b/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleRegistry.java
index d271ecd..b7bc791 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleRegistry.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/ModuleRegistry.java
@@ -17,6 +17,10 @@
 
 package org.apache.ignite.cli.builtins.module;
 
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -24,12 +28,10 @@ import java.util.List;
 import java.util.stream.Collectors;
 import javax.inject.Inject;
 import javax.inject.Singleton;
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonGetter;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.ignite.cli.CliPathsConfigLoader;
 import org.apache.ignite.cli.IgniteCLIException;
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
 
 /**
  * The registry of installed CLI or Ignite server modules.
@@ -152,15 +154,19 @@ public class ModuleRegistry {
      */
     public static class ModuleDefinition {
         /** Module's name. */
+        @IgniteToStringInclude
         public final String name;
 
         /** Module's server artifacts. */
+        @IgniteToStringInclude
         public final List<Path> artifacts;
 
         /** Module's CLI artifacts. */
+        @IgniteToStringInclude
         public final List<Path> cliArtifacts;
 
         /** Type of module source. */
+        @IgniteToStringInclude
         public final SourceType type;
 
         /**
@@ -210,15 +216,8 @@ public class ModuleRegistry {
 
         /** {@inheritDoc} */
         @Override public String toString() {
-            return "ModuleDefinition{" +
-                "name='" + name + '\'' +
-                ", artifacts=" + artifacts +
-                ", cliArtifacts=" + cliArtifacts +
-                ", type=" + type +
-                ", source='" + src + '\'' +
-                '}';
+            return S.toString(ModuleDefinition.class, this);
         }
-
     }
 
     /**
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java b/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java
index 3050cfb..cfd38f3 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/builtins/module/StandardModuleDefinition.java
@@ -19,6 +19,8 @@ package org.apache.ignite.cli.builtins.module;
 
 import java.util.Collections;
 import java.util.List;
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
 
 /**
  * Definition of Ignite standard module.
@@ -27,15 +29,19 @@ import java.util.List;
  */
 public class StandardModuleDefinition {
     /** Module name. **/
+    @IgniteToStringInclude
     public final String name;
 
     /** Module description. */
+    @IgniteToStringInclude
     public final String desc;
 
     /** List of server artifacts. */
+    @IgniteToStringInclude
     public final List<String> artifacts;
 
     /** List of CLI tool artifacts. */
+    @IgniteToStringInclude
     public final List<String> cliArtifacts;
 
     /**
@@ -55,6 +61,6 @@ public class StandardModuleDefinition {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return name + ":\t" + desc;
+        return S.toString(StandardModuleDefinition.class, this);
     }
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
index 28dd677..a91af05 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
+++ b/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
@@ -16,10 +16,12 @@
  */
 package org.apache.ignite.configuration.validation;
 
+import org.apache.ignite.internal.tostring.S;
+
 /** */
 public class ValidationIssue {
     /** */
-    private String message;
+    private final String message;
 
     /** */
     public ValidationIssue(String message) {
@@ -31,8 +33,8 @@ public class ValidationIssue {
         return message;
     }
 
-    /** */
+    /** {@inheritDoc} */
     @Override public String toString() {
-        return "ValidationIssue [message=" + message + ']';
+        return S.toString(ValidationIssue.class, this);
     }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/tostring/CircularStringBuilder.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/CircularStringBuilder.java
new file mode 100644
index 0000000..08a344d
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/CircularStringBuilder.java
@@ -0,0 +1,277 @@
+/*
+ * 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.internal.tostring;
+
+import java.util.Arrays;
+
+/**
+ * Basic string builder over circular buffer.
+ */
+class CircularStringBuilder {
+    /** Value */
+    private final char[] buf;
+
+    /** Writer position (0 if empty). */
+    private int pos;
+
+    /** Value is full flag. */
+    private boolean full;
+
+    /** Number of skipped characters */
+    private int skipped;
+
+    /**
+     * Creates an CircularStringBuilder of the specified capacity.
+     *
+     * @param capacity Buffer capacity.
+     */
+    CircularStringBuilder(int capacity) {
+        assert capacity > 0 : "Can't allocate CircularStringBuilder with capacity: " + capacity;
+
+        buf = new char[capacity];
+        pos = 0;
+        skipped = 0;
+        full = false;
+    }
+
+    /**
+     * Reset internal builder state
+     */
+    public void reset() {
+        Arrays.fill(buf, (char)0);
+
+        pos = 0;
+        full = false;
+        skipped = 0;
+    }
+
+    /**
+     * Returns the length (character count).
+     *
+     * @return the length of the sequence of characters currently
+     * represented by this object
+     */
+    public int length() {
+        return full ? buf.length : pos;
+    }
+
+    /**
+     * Returns the current capacity.
+     *
+     * @return the current capacity
+     */
+    public int capacity() {
+        return buf.length;
+    }
+
+    /**
+     * Appends the string representation of the {@code Object} argument.
+     *
+     * @param obj an {@code Object}.
+     * @return {@code this} for chaining.
+     */
+    public CircularStringBuilder append(Object obj) {
+        return append(String.valueOf(obj));
+    }
+
+    /**
+     * Appends the specified string to this character sequence.
+     *
+     * @param str a string.
+     * @return {@code this} for chaining.
+     */
+    public CircularStringBuilder append(String str) {
+        if (str == null)
+            return appendNull();
+
+        int strLen = str.length();
+
+        if (strLen == 0)
+            return this;
+        else if (strLen >= buf.length) {
+            // String bigger or equal to value length
+            str.getChars(strLen - buf.length, strLen, buf, 0);
+
+            skipped += strLen - buf.length + pos;
+
+            pos = buf.length;
+
+            full = true;
+        }
+        // String is shorter value length
+        else if (buf.length - pos < strLen) {
+            // String doesn't fit into remaining part of value array
+            int firstPart = buf.length - pos;
+
+            if (firstPart > 0)
+                str.getChars(0, firstPart, buf, pos);
+
+            str.getChars(firstPart, strLen, buf, 0);
+
+            skipped += full ? strLen : strLen - firstPart;
+
+            pos = pos + strLen - buf.length;
+
+            full = true;
+        }
+        else {
+            // Whole string fin into remaining part of value array
+            str.getChars(0, strLen, buf, pos);
+
+            skipped += full ? strLen : 0;
+
+            pos += strLen;
+        }
+
+        return this;
+    }
+
+    /**
+     * Append StringBuffer.
+     *
+     * @param sb StringBuffer to append.
+     * @return {@code this} for chaining.
+     */
+    public CircularStringBuilder append(StringBuffer sb) {
+        if (sb == null)
+            return appendNull();
+
+        int strLen = sb.length();
+
+        if (strLen == 0)
+            return this;
+        if (strLen >= buf.length) {
+            // String bigger or equal to value length
+            sb.getChars(strLen - buf.length, strLen, buf, 0);
+
+            skipped += strLen - buf.length + pos;
+
+            pos = buf.length;
+
+            full = true;
+        }
+        // String is shorter value length
+        else if (buf.length - pos < strLen) {
+            // String doesn't fit into remaining part of value array
+            int firstPart = buf.length - pos;
+
+            if (firstPart > 0)
+                sb.getChars(0, firstPart, buf, pos);
+
+            sb.getChars(firstPart, strLen, buf, 0);
+
+            skipped += full ? strLen : strLen - firstPart;
+
+            pos = pos + strLen - buf.length;
+
+            full = true;
+        }
+        else {
+            // Whole string fin into remaining part of value array
+            sb.getChars(0, strLen, buf, pos);
+
+            skipped += full ? strLen : 0;
+
+            pos += strLen;
+        }
+
+        return this;
+    }
+
+    /**
+     * Append StringBuilder.
+     *
+     * @param sb StringBuilder to append.
+     * @return {@code this} for chaining.
+     */
+    public CircularStringBuilder append(StringBuilder sb) {
+        if (sb == null)
+            return appendNull();
+
+        int strLen = sb.length();
+
+        if (strLen == 0)
+            return this;
+        if (strLen >= buf.length) {
+            // String bigger or equal to value length
+            sb.getChars(strLen - buf.length, strLen, buf, 0);
+
+            skipped += strLen - buf.length + pos;
+
+            pos = buf.length;
+
+            full = true;
+        }
+        // String is shorter value length
+        else if (buf.length - pos < strLen) {
+            // String doesn't fit into remaining part of value array
+            int firstPart = buf.length - pos;
+
+            if (firstPart > 0)
+                sb.getChars(0, firstPart, buf, pos);
+
+            sb.getChars(firstPart, strLen, buf, 0);
+
+            skipped += full ? strLen : strLen - firstPart;
+
+            pos = pos + strLen - buf.length;
+
+            full = true;
+        }
+        else {
+            // Whole string fin into remaining part of value array
+            sb.getChars(0, strLen, buf, pos);
+
+            skipped += full ? strLen : 0;
+
+            pos += strLen;
+        }
+
+        return this;
+    }
+
+    /**
+     * @return {@code this} for chaining.
+     */
+    private CircularStringBuilder appendNull() {
+        return append("null");
+    }
+
+    /**
+     * @return Count of skipped elements.
+     */
+    public int getSkipped() {
+        return skipped;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        // Create a copy, don't share the array
+        if (full && pos < buf.length) {
+            char[] tmpBuf = new char[buf.length];
+            int tailLen = buf.length - pos;
+
+            System.arraycopy(buf, pos, tmpBuf, 0, tailLen);
+            System.arraycopy(buf, 0, tmpBuf, tailLen, buf.length - tailLen);
+
+            return new String(tmpBuf, 0, tmpBuf.length);
+        }
+        else
+            return new String(buf, 0, pos);
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/tostring/ClassDescriptor.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/ClassDescriptor.java
new file mode 100644
index 0000000..d61ede9
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/ClassDescriptor.java
@@ -0,0 +1,86 @@
+/*
+ * 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.internal.tostring;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Simple class descriptor containing simple and fully qualified class names as well as
+ * the list of class fields.
+ */
+class ClassDescriptor {
+    /** Class simple name. */
+    private final String sqn;
+
+    /** Class FQN. */
+    private final String fqn;
+
+    /** Class field descriptors. */
+    private final ArrayList<FieldDescriptor> fields = new ArrayList<>();
+
+    /**
+     * @param cls Class.
+     */
+    ClassDescriptor(Class<?> cls) {
+        assert cls != null;
+
+        fqn = cls.getName();
+        sqn = cls.getSimpleName();
+    }
+
+    /**
+     * @param field Field descriptor to be added.
+     */
+    void addField(FieldDescriptor field) {
+        assert field != null;
+
+        fields.add(field);
+    }
+
+    /**
+     *
+     */
+    void sortFields() {
+        fields.trimToSize();
+
+        fields.sort(Comparator.comparingInt(FieldDescriptor::getOrder));
+    }
+
+    /**
+     * @return Simple class name.
+     */
+    String getSimpleClassName() {
+        return sqn;
+    }
+
+    /**
+     * @return Fully qualified class name.
+     */
+    String getFullyQualifiedClassName() {
+        return fqn;
+    }
+
+    /**
+     * @return List of fields.
+     */
+    List<FieldDescriptor> getFields() {
+        return fields;
+    }
+}
\ No newline at end of file
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/tostring/FieldDescriptor.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/FieldDescriptor.java
new file mode 100644
index 0000000..683fae5
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/FieldDescriptor.java
@@ -0,0 +1,142 @@
+/*
+ * 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.internal.tostring;
+
+import java.lang.invoke.VarHandle;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import org.intellij.lang.annotations.MagicConstant;
+
+/**
+ * Simple field descriptor containing field name and its order in the class descriptor.
+ */
+class FieldDescriptor {
+    /** */
+    public static final int FIELD_TYPE_OBJECT = 0;
+
+    /** */
+    public static final int FIELD_TYPE_BYTE = 1;
+
+    /** */
+    public static final int FIELD_TYPE_BOOLEAN = 2;
+
+    /** */
+    public static final int FIELD_TYPE_CHAR = 3;
+
+    /** */
+    public static final int FIELD_TYPE_SHORT = 4;
+
+    /** */
+    public static final int FIELD_TYPE_INT = 5;
+
+    /** */
+    public static final int FIELD_TYPE_FLOAT = 6;
+
+    /** */
+    public static final int FIELD_TYPE_LONG = 7;
+
+    /** */
+    public static final int FIELD_TYPE_DOUBLE = 8;
+
+    /** Field name. */
+    private final String name;
+
+    /** */
+    private int order = Integer.MAX_VALUE;
+
+    /** Field VarHandle. */
+    private final VarHandle varHandle;
+
+    /** Numeric constant for the field's type. One of {@code FIELD_TYPE_*} constants of current class. */
+    private final int type;
+
+    /** Class of the field. Upper bound in case of generic field types. */
+    private final Class<?> cls;
+
+    /**
+     * @param field Field descriptor.
+     * @param varHandle Field VarHandle.
+     */
+    FieldDescriptor(Field field, VarHandle varHandle) {
+        assert (field.getModifiers() & Modifier.STATIC) == 0 : "Static fields are not allowed here: " + field;
+
+        this.varHandle = varHandle;
+
+        cls = field.getType();
+
+        name = field.getName();
+
+        if (!cls.isPrimitive())
+            type = FIELD_TYPE_OBJECT;
+        else {
+            if (cls == byte.class)
+                type = FIELD_TYPE_BYTE;
+            else if (cls == boolean.class)
+                type = FIELD_TYPE_BOOLEAN;
+            else if (cls == char.class)
+                type = FIELD_TYPE_CHAR;
+            else if (cls == short.class)
+                type = FIELD_TYPE_SHORT;
+            else if (cls == int.class)
+                type = FIELD_TYPE_INT;
+            else if (cls == float.class)
+                type = FIELD_TYPE_FLOAT;
+            else if (cls == long.class)
+                type = FIELD_TYPE_LONG;
+            else if (cls == double.class)
+                type = FIELD_TYPE_DOUBLE;
+            else
+                throw new IllegalArgumentException("Unexpected primitive type: " + cls);
+        }
+    }
+
+    /**
+     * @return Field order.
+     */
+    int getOrder() { return order; }
+
+    /**
+     * @param order Field order.
+     */
+    void setOrder(int order) { this.order = order; }
+
+    /**
+     * @return Field VarHandle.
+     */
+    public VarHandle varHandle() {
+        return varHandle;
+    }
+
+    /**
+     * @return Numeric constant for the field's type. One of {@code FIELD_TYPE_*} constants of current class.
+     */
+    @MagicConstant(valuesFromClass = FieldDescriptor.class)
+    public int type() {
+        return type;
+    }
+
+    /** */
+    public Class<?> fieldClass() {
+        return cls;
+    }
+
+    /**
+     * @return Field name.
+     */
+    String getName() { return name; }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringBuilder.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringBuilder.java
new file mode 100644
index 0000000..a990d2e
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringBuilder.java
@@ -0,0 +1,2110 @@
+/*
+ * 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.internal.tostring;
+
+import java.io.Externalizable;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.EventListener;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.apache.ignite.lang.IgniteInternalException;
+import org.apache.ignite.lang.IgniteStringBuilder;
+import org.apache.ignite.lang.IgniteSystemProperties;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static java.util.Objects.nonNull;
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_SENSITIVE_DATA_LOGGING;
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_TO_STRING_COLLECTION_LIMIT;
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_TO_STRING_IGNORE_RUNTIME_EXCEPTION;
+import static org.apache.ignite.lang.IgniteSystemProperties.getBoolean;
+import static org.apache.ignite.lang.IgniteSystemProperties.getInteger;
+import static org.apache.ignite.lang.IgniteSystemProperties.getString;
+
+/**
+ * Provides auto-generation framework for {@code toString()} output.
+ * <p>
+ * In case of recursion, object fields will be printed only for the first entry to prevent recursion,
+ * and all the next repeated entrances will be shown as "ClassName@hash".
+ * <p>
+ * Default exclusion policy (can be overridden with {@link IgniteToStringInclude} annotation):
+ * <ul>
+ * <li>fields with {@link IgniteToStringExclude} annotation
+ * <li>classes with {@link IgniteToStringExclude} annotation
+ * <li>static fields
+ * <li>non-private fields
+ * <li>arrays
+ * <li>fields of type {@link Object}
+ * <li>fields of type {@link Thread}
+ * <li>fields of type {@link Runnable}
+ * <li>fields of type {@link Serializable}
+ * <li>fields of type {@link Externalizable}
+ * <li>{@link InputStream} implementations
+ * <li>{@link OutputStream} implementations
+ * <li>{@link EventListener} implementations
+ * <li>{@link Lock} implementations
+ * <li>{@link ReadWriteLock} implementations
+ * <li>{@link Condition} implementations
+ * <li>{@link Map} implementations
+ * <li>{@link Collection} implementations
+ * </ul>
+ */
+@SuppressWarnings({"BooleanParameter", "NonFinalUtilityClass"})
+public class IgniteToStringBuilder {
+    /** Empty array instance. */
+    private static final Object[] EMPTY_ARRAY = new Object[0];
+
+    /** Max collection elements to be written. */
+    private static final int COLLECTION_LIMIT = getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, 100);
+
+    /** Ignore flag for runtime exceptions while building string. */
+    private static final boolean IGNORE_RUNTIME_EXCEPTION = !getBoolean(IGNITE_TO_STRING_IGNORE_RUNTIME_EXCEPTION, false);
+
+    /** Supplier for {@link #includeSensitive} with default behavior. */
+    private static final AtomicReference<Supplier<SensitiveDataLoggingPolicy>> SENS_DATA_LOG_SUP_REF =
+        new AtomicReference<>(new Supplier<>() {
+            /** Sensitive data logging policy. */
+            final SensitiveDataLoggingPolicy sensitiveDataLoggingPolicy =
+                SensitiveDataLoggingPolicy.valueOf(getString(IGNITE_SENSITIVE_DATA_LOGGING, "hash").toUpperCase());
+
+            /** {@inheritDoc} */
+            @Override public SensitiveDataLoggingPolicy get() {
+                return sensitiveDataLoggingPolicy;
+            }
+        });
+
+    /** Every thread has its own string builder. */
+    private static final ThreadLocal<SBLimitedLength> threadLocSB = ThreadLocal.withInitial(() ->
+        new SBLimitedLength(256));
+
+    /**
+     * Tracks the objects currently printing in the string builder.
+     * <p>
+     * Since {@code toString()} methods can be chain-called from the same thread we
+     * have to keep a map of this objects pointed to the position of previous occurrence
+     * and remove/add them in each {@code toString(...)} apply.
+     */
+    private static final ThreadLocal<IdentityHashMap<Object, EntryReference>> savedObjects = ThreadLocal.withInitial(IdentityHashMap::new);
+
+    /**
+     *
+     */
+    private static final Map<String, ClassDescriptor> classCache = new ConcurrentHashMap<>();
+
+    /**
+     * Initialization-on-demand holder.
+     *
+     * @see <a href= "https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom">
+     * "Initialization-on-demand holder idiom"</a>.
+     */
+    private static final class Holder {
+        /** Supplier holder for {@link #includeSensitive} and {@link #getSensitiveDataLogging}. */
+        static final Supplier<SensitiveDataLoggingPolicy> SENS_DATA_LOG_SUP = SENS_DATA_LOG_SUP_REF.get();
+    }
+
+    /**
+     * @return {@link SensitiveDataLoggingPolicy} Log levels for sensitive data
+     */
+    public static SensitiveDataLoggingPolicy getSensitiveDataLogging() {
+        return Holder.SENS_DATA_LOG_SUP.get();
+    }
+
+    /**
+     * Setting the logic of the {@link #includeSensitive} and {@link #getSensitiveDataLogging} methods.
+     * <p>
+     * Overrides default supplier that uses {@link IgniteSystemProperties#IGNITE_SENSITIVE_DATA_LOGGING}
+     * system property.
+     *
+     * <b>Important!</b> Changing the logic is possible only until the first
+     * call of {@link #includeSensitive} or {@link #getSensitiveDataLogging} methods.
+     *
+     * @param sup {@link SensitiveDataLoggingPolicy} supplier.
+     */
+    public static void setSensitiveDataLoggingPolicySupplier(Supplier<SensitiveDataLoggingPolicy> sup) {
+        assert nonNull(sup);
+
+        SENS_DATA_LOG_SUP_REF.set(sup);
+    }
+
+    /**
+     * Return include sensitive data flag.
+     *
+     * @return {@code true} if need to include sensitive data, {@code false} otherwise.
+     * @see IgniteToStringBuilder#setSensitiveDataLoggingPolicySupplier(Supplier)
+     */
+    public static boolean includeSensitive() {
+        return Holder.SENS_DATA_LOG_SUP.get() == SensitiveDataLoggingPolicy.PLAIN;
+    }
+
+    /**
+     * @param obj Object.
+     * @return Hexed identity hashcode.
+     */
+    public static String identity(Object obj) {
+        return '@' + Integer.toHexString(System.identityHashCode(obj));
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @param name4 Additional parameter name.
+     * @param val4 Additional parameter value.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0,
+        String name1, Object val1,
+        String name2, Object val2,
+        String name3, Object val3,
+        String name4, Object val4) {
+        return toString(cls,
+            obj,
+            name0, val0, false,
+            name1, val1, false,
+            name2, val2, false,
+            name3, val3, false,
+            name4, val4, false);
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @param name4 Additional parameter name.
+     * @param val4 Additional parameter value.
+     * @param name5 Additional parameter name.
+     * @param val5 Additional parameter value.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0,
+        String name1, Object val1,
+        String name2, Object val2,
+        String name3, Object val3,
+        String name4, Object val4,
+        String name5, Object val5) {
+        return toString(cls,
+            obj,
+            name0, val0, false,
+            name1, val1, false,
+            name2, val2, false,
+            name3, val3, false,
+            name4, val4, false,
+            name5, val5, false);
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @param name4 Additional parameter name.
+     * @param val4 Additional parameter value.
+     * @param name5 Additional parameter name.
+     * @param val5 Additional parameter value.
+     * @param name6 Additional parameter name.
+     * @param val6 Additional parameter value.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0,
+        String name1, Object val1,
+        String name2, Object val2,
+        String name3, Object val3,
+        String name4, Object val4,
+        String name5, Object val5,
+        String name6, Object val6) {
+        return toString(cls,
+            obj,
+            name0, val0, false,
+            name1, val1, false,
+            name2, val2, false,
+            name3, val3, false,
+            name4, val4, false,
+            name5, val5, false,
+            name6, val6, false);
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @param sens3 Property sensitive flag.
+     * @param name4 Additional parameter name.
+     * @param val4 Additional parameter value.
+     * @param sens4 Property sensitive flag.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0, boolean sens0,
+        String name1, Object val1, boolean sens1,
+        String name2, Object val2, boolean sens2,
+        String name3, Object val3, boolean sens3,
+        String name4, Object val4, boolean sens4) {
+        assert cls != null;
+        assert obj != null;
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+        assert name4 != null;
+
+        Object[] addNames = new Object[5];
+        Object[] addVals = new Object[5];
+        boolean[] addSens = new boolean[5];
+
+        addNames[0] = name0;
+        addVals[0] = val0;
+        addSens[0] = sens0;
+        addNames[1] = name1;
+        addVals[1] = val1;
+        addSens[1] = sens1;
+        addNames[2] = name2;
+        addVals[2] = val2;
+        addSens[2] = sens2;
+        addNames[3] = name3;
+        addVals[3] = val3;
+        addSens[3] = sens3;
+        addNames[4] = name4;
+        addVals[4] = val4;
+        addSens[4] = sens4;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 5);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @param sens3 Property sensitive flag.
+     * @param name4 Additional parameter name.
+     * @param val4 Additional parameter value.
+     * @param sens4 Property sensitive flag.
+     * @param name5 Additional parameter name.
+     * @param val5 Additional parameter value.
+     * @param sens5 Property sensitive flag.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0, boolean sens0,
+        String name1, Object val1, boolean sens1,
+        String name2, Object val2, boolean sens2,
+        String name3, Object val3, boolean sens3,
+        String name4, Object val4, boolean sens4,
+        String name5, Object val5, boolean sens5) {
+        assert cls != null;
+        assert obj != null;
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+        assert name4 != null;
+        assert name5 != null;
+
+        Object[] addNames = new Object[6];
+        Object[] addVals = new Object[6];
+        boolean[] addSens = new boolean[6];
+
+        addNames[0] = name0;
+        addVals[0] = val0;
+        addSens[0] = sens0;
+        addNames[1] = name1;
+        addVals[1] = val1;
+        addSens[1] = sens1;
+        addNames[2] = name2;
+        addVals[2] = val2;
+        addSens[2] = sens2;
+        addNames[3] = name3;
+        addVals[3] = val3;
+        addSens[3] = sens3;
+        addNames[4] = name4;
+        addVals[4] = val4;
+        addSens[4] = sens4;
+        addNames[5] = name5;
+        addVals[5] = val5;
+        addSens[5] = sens5;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 6);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @param sens3 Property sensitive flag.
+     * @param name4 Additional parameter name.
+     * @param val4 Additional parameter value.
+     * @param sens4 Property sensitive flag.
+     * @param name5 Additional parameter name.
+     * @param val5 Additional parameter value.
+     * @param sens5 Property sensitive flag.
+     * @param name6 Additional parameter name.
+     * @param val6 Additional parameter value.
+     * @param sens6 Property sensitive flag.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0, boolean sens0,
+        String name1, Object val1, boolean sens1,
+        String name2, Object val2, boolean sens2,
+        String name3, Object val3, boolean sens3,
+        String name4, Object val4, boolean sens4,
+        String name5, Object val5, boolean sens5,
+        String name6, Object val6, boolean sens6) {
+        assert cls != null;
+        assert obj != null;
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+        assert name4 != null;
+        assert name5 != null;
+        assert name6 != null;
+
+        Object[] addNames = new Object[7];
+        Object[] addVals = new Object[7];
+        boolean[] addSens = new boolean[7];
+
+        addNames[0] = name0;
+        addVals[0] = val0;
+        addSens[0] = sens0;
+        addNames[1] = name1;
+        addVals[1] = val1;
+        addSens[1] = sens1;
+        addNames[2] = name2;
+        addVals[2] = val2;
+        addSens[2] = sens2;
+        addNames[3] = name3;
+        addVals[3] = val3;
+        addSens[3] = sens3;
+        addNames[4] = name4;
+        addVals[4] = val4;
+        addSens[4] = sens4;
+        addNames[5] = name5;
+        addVals[5] = val5;
+        addSens[5] = sens5;
+        addNames[6] = name6;
+        addVals[6] = val6;
+        addSens[6] = sens6;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 7);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0,
+        String name1, Object val1,
+        String name2, Object val2,
+        String name3, Object val3) {
+        return toString(cls, obj,
+            name0, val0, false,
+            name1, val1, false,
+            name2, val2, false,
+            name3, val3, false);
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Additional parameter name.
+     * @param val3 Additional parameter value.
+     * @param sens3 Property sensitive flag.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0, boolean sens0,
+        String name1, Object val1, boolean sens1,
+        String name2, Object val2, boolean sens2,
+        String name3, Object val3, boolean sens3) {
+        assert cls != null;
+        assert obj != null;
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+
+        Object[] addNames = new Object[4];
+        Object[] addVals = new Object[4];
+        boolean[] addSens = new boolean[4];
+
+        addNames[0] = name0;
+        addVals[0] = val0;
+        addSens[0] = sens0;
+        addNames[1] = name1;
+        addVals[1] = val1;
+        addSens[1] = sens1;
+        addNames[2] = name2;
+        addVals[2] = val2;
+        addSens[2] = sens2;
+        addNames[3] = name3;
+        addVals[3] = val3;
+        addSens[3] = sens3;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 4);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0,
+        String name1, Object val1,
+        String name2, Object val2) {
+        return toString(cls,
+            obj,
+            name0, val0, false,
+            name1, val1, false,
+            name2, val2, false);
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Additional parameter name.
+     * @param val2 Additional parameter value.
+     * @param sens2 Property sensitive flag.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0, boolean sens0,
+        String name1, Object val1, boolean sens1,
+        String name2, Object val2, boolean sens2) {
+        assert cls != null;
+        assert obj != null;
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+
+        Object[] addNames = new Object[3];
+        Object[] addVals = new Object[3];
+        boolean[] addSens = new boolean[3];
+
+        addNames[0] = name0;
+        addVals[0] = val0;
+        addSens[0] = sens0;
+        addNames[1] = name1;
+        addVals[1] = val1;
+        addSens[1] = sens1;
+        addNames[2] = name2;
+        addVals[2] = val2;
+        addSens[2] = sens2;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 3);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0,
+        String name1, Object val1) {
+        return toString(cls, obj, name0, val0, false, name1, val1, false);
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name0 Additional parameter name.
+     * @param val0 Additional parameter value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Additional parameter name.
+     * @param val1 Additional parameter value.
+     * @param sens1 Property sensitive flag.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj,
+        String name0, Object val0, boolean sens0,
+        String name1, Object val1, boolean sens1) {
+        assert cls != null;
+        assert obj != null;
+        assert name0 != null;
+        assert name1 != null;
+
+        Object[] addNames = new Object[2];
+        Object[] addVals = new Object[2];
+        boolean[] addSens = new boolean[2];
+
+        addNames[0] = name0;
+        addVals[0] = val0;
+        addSens[0] = sens0;
+        addNames[1] = name1;
+        addVals[1] = val1;
+        addSens[1] = sens1;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 2);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name Additional parameter name.
+     * @param val Additional parameter value.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj, String name, @Nullable Object val) {
+        return toString(cls, obj, name, val, false);
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param name Additional parameter name.
+     * @param val Additional parameter value.
+     * @param sens Property sensitive flag.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj, String name, @Nullable Object val, boolean sens) {
+        assert cls != null;
+        assert obj != null;
+        assert name != null;
+
+        Object[] addNames = new Object[1];
+        Object[] addVals = new Object[1];
+        boolean[] addSens = new boolean[1];
+
+        addNames[0] = name;
+        addVals[0] = val;
+        addSens[0] = sens;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 1);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj) {
+        assert cls != null;
+        assert obj != null;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(cls, sb, obj, EMPTY_ARRAY, EMPTY_ARRAY, null, 0);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces auto-generated output of string presentation for given object and its declaration class.
+     *
+     * @param <T> Type of the object.
+     * @param cls Declaration class of the object. Note that this should not be a runtime class.
+     * @param obj Object to get a string presentation for.
+     * @param parent String representation of parent.
+     * @return String presentation of the given object.
+     */
+    public static <T> String toString(Class<T> cls, T obj, String parent) {
+        return parent != null ? toString(cls, obj, "super", parent) : toString(cls, obj);
+    }
+
+    /**
+     * Print value with length limitation.
+     *
+     * @param buf buffer to print to.
+     * @param val value to print, can be {@code null}.
+     */
+    private static void toString(SBLimitedLength buf, Object val) {
+        toString(buf, null, val);
+    }
+
+    /**
+     * Print value with length limitation.
+     *
+     * @param buf buffer to print to.
+     * @param cls value class.
+     * @param val value to print.
+     */
+    private static void toString(SBLimitedLength buf, Class<?> cls, Object val) {
+        if (val == null) {
+            buf.a("null");
+
+            return;
+        }
+
+        if (cls == null)
+            cls = val.getClass();
+
+        if (cls.isPrimitive()) {
+            buf.a(val);
+
+            return;
+        }
+
+        IdentityHashMap<Object, EntryReference> svdObjs = savedObjects.get();
+
+        if (handleRecursion(buf, val, cls, svdObjs))
+            return;
+
+        svdObjs.put(val, new EntryReference(buf.length()));
+
+        try {
+            if (cls.isArray())
+                addArray(buf, cls, val);
+            else if (val instanceof Collection)
+                addCollection(buf, (Collection<?>)val);
+            else if (val instanceof Map)
+                addMap(buf, (Map<?, ?>)val);
+            else
+                buf.a(val);
+        }
+        finally {
+            svdObjs.remove(val);
+        }
+    }
+
+    /**
+     * Writes array to buffer.
+     *
+     * @param buf String builder buffer.
+     * @param arrType Type of the array.
+     * @param obj Array object.
+     */
+    private static void addArray(SBLimitedLength buf, Class arrType, Object obj) {
+        if (arrType.getComponentType().isPrimitive()) {
+            buf.a(arrayToString(obj));
+
+            return;
+        }
+
+        Object[] arr = (Object[])obj;
+
+        buf.a(arrType.getSimpleName()).a(" [");
+
+        for (int i = 0; i < arr.length; i++) {
+            toString(buf, arr[i]);
+
+            if (i == COLLECTION_LIMIT - 1 || i == arr.length - 1)
+                break;
+
+            buf.a(", ");
+        }
+
+        handleOverflow(buf, arr.length);
+
+        buf.a(']');
+    }
+
+    /**
+     * Writes collection to buffer.
+     *
+     * @param buf String builder buffer.
+     * @param col Collection object.
+     */
+    private static void addCollection(SBLimitedLength buf, Collection<?> col) {
+        buf.a(col.getClass().getSimpleName()).a(" [");
+
+        int cnt = 0;
+        boolean needHandleOverflow = true;
+
+        Iterator<?> iter = col.iterator();
+        int colSize = col.size();
+
+        while (iter.hasNext()) {
+            Object obj;
+
+            try {
+                obj = iter.next();
+            }
+            catch (ConcurrentModificationException e) {
+                handleConcurrentModification(buf, cnt, colSize);
+
+                needHandleOverflow = false;
+                break;
+            }
+
+            toString(buf, obj);
+
+            if (++cnt == COLLECTION_LIMIT || cnt == colSize)
+                break;
+
+            buf.a(", ");
+        }
+
+        if (needHandleOverflow)
+            handleOverflow(buf, colSize);
+
+        buf.a(']');
+    }
+
+    /**
+     * Writes map to buffer.
+     *
+     * @param buf String builder buffer.
+     * @param map Map object.
+     */
+    private static <K, V> void addMap(SBLimitedLength buf, Map<K, V> map) {
+        buf.a(map.getClass().getSimpleName()).a(" {");
+
+        int cnt = 0;
+        boolean needHandleOverflow = true;
+
+        Iterator<Map.Entry<K, V>> iter = map.entrySet().iterator();
+        int mapSize = map.size();
+
+        while (iter.hasNext()) {
+            Object key;
+            Object value;
+
+            try {
+                Map.Entry<K, V> entry = iter.next();
+
+                key = entry.getKey();
+                value = entry.getValue();
+            }
+            catch (ConcurrentModificationException e) {
+                handleConcurrentModification(buf, cnt, mapSize);
+
+                needHandleOverflow = false;
+                break;
+            }
+
+            toString(buf, key);
+
+            buf.a('=');
+
+            toString(buf, value);
+
+            if (++cnt == COLLECTION_LIMIT || cnt == mapSize)
+                break;
+
+            buf.a(", ");
+        }
+
+        if (needHandleOverflow)
+            handleOverflow(buf, mapSize);
+
+        buf.a('}');
+    }
+
+    /**
+     * Writes overflow message to buffer if needed.
+     *
+     * @param buf String builder buffer.
+     * @param size Size to compare with limit.
+     */
+    private static void handleOverflow(SBLimitedLength buf, int size) {
+        int overflow = size - COLLECTION_LIMIT;
+
+        if (overflow > 0)
+            buf.a("... and ").a(overflow).a(" more");
+    }
+
+    /**
+     * Writes message about situation of ConcurrentModificationException caught when iterating over collection.
+     *
+     * @param buf String builder buffer.
+     * @param writtenElements Number of elements successfully written to output.
+     * @param size Overall size of collection.
+     */
+    private static void handleConcurrentModification(SBLimitedLength buf, int writtenElements, int size) {
+        buf.a("... concurrent modification was detected, ").a(writtenElements).a(" out of ").a(size)
+            .a(" were written");
+    }
+
+    /**
+     * Creates an uniformed string presentation for the given object.
+     *
+     * @param <T> Type of object.
+     * @param cls Class of the object.
+     * @param buf String builder buffer.
+     * @param obj Object for which to get string presentation.
+     * @param addNames Names of additional values to be included.
+     * @param addVals Additional values to be included.
+     * @param addSens Sensitive flag of values or {@code null} if all values are not sensitive.
+     * @param addLen How many additional values will be included.
+     * @return String presentation of the given object.
+     */
+    private static <T> String toStringImpl(
+        Class<T> cls,
+        SBLimitedLength buf,
+        T obj,
+        Object[] addNames,
+        Object[] addVals,
+        @Nullable boolean[] addSens,
+        int addLen) {
+        assert cls != null;
+        assert buf != null;
+        assert obj != null;
+        assert addNames != null;
+        assert addVals != null;
+        assert addNames.length == addVals.length;
+        assert addLen <= addNames.length;
+
+        boolean newStr = buf.length() == 0;
+
+        IdentityHashMap<Object, EntryReference> svdObjs = savedObjects.get();
+
+        if (newStr)
+            svdObjs.put(obj, new EntryReference(buf.length()));
+
+        try {
+            int len = buf.length();
+
+            String s = toStringImpl0(cls, buf, obj, addNames, addVals, addSens, addLen);
+
+            if (newStr)
+                return s;
+
+            buf.setLength(len);
+
+            return s.substring(len);
+        }
+        finally {
+            if (newStr)
+                svdObjs.remove(obj);
+        }
+    }
+
+    /**
+     * Creates an uniformed string presentation for the given object.
+     *
+     * @param cls Class of the object.
+     * @param buf String builder buffer.
+     * @param obj Object for which to get string presentation.
+     * @param addNames Names of additional values to be included.
+     * @param addVals Additional values to be included.
+     * @param addSens Sensitive flag of values or {@code null} if all values are not sensitive.
+     * @param addLen How many additional values will be included.
+     * @param <T> Type of object.
+     * @return String presentation of the given object.
+     */
+    private static <T> String toStringImpl0(
+        Class<T> cls,
+        SBLimitedLength buf,
+        T obj,
+        Object[] addNames,
+        Object[] addVals,
+        @Nullable boolean[] addSens,
+        int addLen
+    ) {
+        try {
+            ClassDescriptor cd = getClassDescriptor(cls);
+
+            assert cd != null;
+
+            buf.a(cd.getSimpleClassName());
+
+            EntryReference ref = savedObjects.get().get(obj);
+
+            if (ref != null && ref.hashNeeded) {
+                buf.a(identity(obj));
+
+                ref.hashNeeded = false;
+            }
+
+            buf.a(" [");
+
+            boolean first = true;
+
+            for (FieldDescriptor fd : cd.getFields()) {
+                if (!first)
+                    buf.a(", ");
+                else
+                    first = false;
+
+                buf.a(fd.getName()).a('=');
+
+                final VarHandle fH = fd.varHandle();
+
+                switch (fd.type()) {
+                    case FieldDescriptor.FIELD_TYPE_OBJECT:
+                        try {
+                            toString(buf, fd.fieldClass(), fH.get(obj));
+                        }
+                        catch (RuntimeException e) {
+                            if (IGNORE_RUNTIME_EXCEPTION) {
+                                buf.a("Runtime exception was caught when building string representation: " +
+                                    e.getMessage());
+                            }
+                            else
+                                throw e;
+                        }
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_BYTE:
+                        buf.a((byte)fH.get(obj));
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_BOOLEAN:
+                        buf.a((boolean)fH.get(obj));
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_CHAR:
+                        buf.a((char)fH.get(obj));
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_SHORT:
+                        buf.a((short)fH.get(obj));
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_INT:
+                        buf.a((int)fH.get(obj));
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_FLOAT:
+                        buf.a((float)fH.get(obj));
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_LONG:
+                        buf.a((long)fH.get(obj));
+
+                        break;
+                    case FieldDescriptor.FIELD_TYPE_DOUBLE:
+                        buf.a((double)fH.get(obj));
+
+                        break;
+                }
+            }
+
+            appendVals(buf, first, addNames, addVals, addSens, addLen);
+
+            buf.a(']');
+
+            return buf.toString();
+        }
+        // Specifically catching all exceptions.
+        catch (Exception e) {
+            // Remove entry from cache to avoid potential memory leak
+            // in case new class loader got loaded under the same identity hash.
+            classCache.remove(cls.getName() + System.identityHashCode(cls.getClassLoader()));
+
+            // No other option here.
+            throw new IgniteInternalException(e);
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name Property name.
+     * @param val Property value.
+     * @return String presentation.
+     */
+    public static String toString(String str, String name, @Nullable Object val) {
+        return toString(str, name, val, false);
+    }
+
+    /**
+     * Returns limited string representation of array.
+     *
+     * @param arr Array object. Each value is automatically wrapped if it has a primitive type.
+     * @return String representation of an array.
+     */
+    public static String arrayToString(Object arr) {
+        if (arr == null)
+            return "null";
+
+        String res;
+
+        int arrLen;
+
+        if (arr instanceof Object[]) {
+            Object[] objArr = (Object[])arr;
+
+            arrLen = objArr.length;
+
+            if (arrLen > COLLECTION_LIMIT)
+                objArr = Arrays.copyOf(objArr, COLLECTION_LIMIT);
+
+            res = Arrays.toString(objArr);
+        }
+        else {
+            res = toStringWithLimit(arr, COLLECTION_LIMIT);
+
+            arrLen = Array.getLength(arr);
+        }
+
+        if (arrLen > COLLECTION_LIMIT) {
+            StringBuilder resSB = new StringBuilder(res);
+
+            resSB.deleteCharAt(resSB.length() - 1);
+
+            resSB.append("... and ").append(arrLen - COLLECTION_LIMIT).append(" more]");
+
+            res = resSB.toString();
+        }
+
+        return res;
+    }
+
+    /**
+     * Returns limited string representation of array.
+     *
+     * @param arr Input array. Each value is automatically wrapped if it has a primitive type.
+     * @param limit max array items to string limit.
+     * @return String representation of an array.
+     */
+    private static String toStringWithLimit(Object arr, int limit) {
+        int arrIdxMax = Array.getLength(arr) - 1;
+
+        if (arrIdxMax == -1)
+            return "[]";
+
+        int idxMax = Math.min(arrIdxMax, limit);
+
+        StringBuilder b = new StringBuilder();
+
+        b.append('[');
+
+        for (int i = 0; i <= idxMax; ++i) {
+            b.append(Array.get(arr, i));
+
+            if (i == idxMax)
+                return b.append(']').toString();
+
+            b.append(", ");
+        }
+
+        return b.toString();
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name Property name.
+     * @param val Property value.
+     * @param sens Property sensitive flag.
+     * @return String presentation.
+     */
+    public static String toString(String str, String name, @Nullable Object val, boolean sens) {
+        assert name != null;
+
+        Object[] propNames = new Object[1];
+        Object[] propVals = new Object[1];
+        boolean[] propSens = new boolean[1];
+
+        propNames[0] = name;
+        propVals[0] = val;
+        propSens[0] = sens;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, 1);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @return String presentation.
+     */
+    public static String toString(String str, String name0, @Nullable Object val0, String name1,
+        @Nullable Object val1) {
+        return toString(str, name0, val0, false, name1, val1, false);
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @param name2 Property name.
+     * @param val2 Property value.
+     * @return String presentation.
+     */
+    public static String toString(String str, String name0, @Nullable Object val0, String name1,
+        @Nullable Object val1, String name2, @Nullable Object val2) {
+        return toString(str, name0, val0, false, name1, val1, false, name2, val2, false);
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @param sens1 Property sensitive flag.
+     * @return String presentation.
+     */
+    public static String toString(String str,
+        String name0, @Nullable Object val0, boolean sens0,
+        String name1, @Nullable Object val1, boolean sens1) {
+        assert name0 != null;
+        assert name1 != null;
+
+        Object[] propNames = new Object[2];
+        Object[] propVals = new Object[2];
+        boolean[] propSens = new boolean[2];
+
+        propNames[0] = name0;
+        propVals[0] = val0;
+        propSens[0] = sens0;
+        propNames[1] = name1;
+        propVals[1] = val1;
+        propSens[1] = sens1;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, 2);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Property name.
+     * @param val2 Property value.
+     * @param sens2 Property sensitive flag.
+     * @return String presentation.
+     */
+    public static String toString(String str,
+        String name0, @Nullable Object val0, boolean sens0,
+        String name1, @Nullable Object val1, boolean sens1,
+        String name2, @Nullable Object val2, boolean sens2) {
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+
+        Object[] propNames = new Object[3];
+        Object[] propVals = new Object[3];
+        boolean[] propSens = new boolean[3];
+
+        propNames[0] = name0;
+        propVals[0] = val0;
+        propSens[0] = sens0;
+        propNames[1] = name1;
+        propVals[1] = val1;
+        propSens[1] = sens1;
+        propNames[2] = name2;
+        propVals[2] = val2;
+        propSens[2] = sens2;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, 3);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Property name.
+     * @param val2 Property value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Property name.
+     * @param val3 Property value.
+     * @param sens3 Property sensitive flag.
+     * @return String presentation.
+     */
+    public static String toString(String str,
+        String name0, @Nullable Object val0, boolean sens0,
+        String name1, @Nullable Object val1, boolean sens1,
+        String name2, @Nullable Object val2, boolean sens2,
+        String name3, @Nullable Object val3, boolean sens3) {
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+
+        Object[] propNames = new Object[4];
+        Object[] propVals = new Object[4];
+        boolean[] propSens = new boolean[4];
+
+        propNames[0] = name0;
+        propVals[0] = val0;
+        propSens[0] = sens0;
+        propNames[1] = name1;
+        propVals[1] = val1;
+        propSens[1] = sens1;
+        propNames[2] = name2;
+        propVals[2] = val2;
+        propSens[2] = sens2;
+        propNames[3] = name3;
+        propVals[3] = val3;
+        propSens[3] = sens3;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, 4);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Property name.
+     * @param val2 Property value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Property name.
+     * @param val3 Property value.
+     * @param sens3 Property sensitive flag.
+     * @param name4 Property name.
+     * @param val4 Property value.
+     * @param sens4 Property sensitive flag.
+     * @return String presentation.
+     */
+    public static String toString(String str,
+        String name0, @Nullable Object val0, boolean sens0,
+        String name1, @Nullable Object val1, boolean sens1,
+        String name2, @Nullable Object val2, boolean sens2,
+        String name3, @Nullable Object val3, boolean sens3,
+        String name4, @Nullable Object val4, boolean sens4) {
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+        assert name4 != null;
+
+        Object[] propNames = new Object[5];
+        Object[] propVals = new Object[5];
+        boolean[] propSens = new boolean[5];
+
+        propNames[0] = name0;
+        propVals[0] = val0;
+        propSens[0] = sens0;
+        propNames[1] = name1;
+        propVals[1] = val1;
+        propSens[1] = sens1;
+        propNames[2] = name2;
+        propVals[2] = val2;
+        propSens[2] = sens2;
+        propNames[3] = name3;
+        propVals[3] = val3;
+        propSens[3] = sens3;
+        propNames[4] = name4;
+        propVals[4] = val4;
+        propSens[4] = sens4;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, 5);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Property name.
+     * @param val2 Property value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Property name.
+     * @param val3 Property value.
+     * @param sens3 Property sensitive flag.
+     * @param name4 Property name.
+     * @param val4 Property value.
+     * @param sens4 Property sensitive flag.
+     * @param name5 Property name.
+     * @param val5 Property value.
+     * @param sens5 Property sensitive flag.
+     * @return String presentation.
+     */
+    public static String toString(String str,
+        String name0, @Nullable Object val0, boolean sens0,
+        String name1, @Nullable Object val1, boolean sens1,
+        String name2, @Nullable Object val2, boolean sens2,
+        String name3, @Nullable Object val3, boolean sens3,
+        String name4, @Nullable Object val4, boolean sens4,
+        String name5, @Nullable Object val5, boolean sens5) {
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+        assert name4 != null;
+        assert name5 != null;
+
+        Object[] propNames = new Object[6];
+        Object[] propVals = new Object[6];
+        boolean[] propSens = new boolean[6];
+
+        propNames[0] = name0;
+        propVals[0] = val0;
+        propSens[0] = sens0;
+        propNames[1] = name1;
+        propVals[1] = val1;
+        propSens[1] = sens1;
+        propNames[2] = name2;
+        propVals[2] = val2;
+        propSens[2] = sens2;
+        propNames[3] = name3;
+        propVals[3] = val3;
+        propSens[3] = sens3;
+        propNames[4] = name4;
+        propVals[4] = val4;
+        propSens[4] = sens4;
+        propNames[5] = name5;
+        propVals[5] = val5;
+        propSens[5] = sens5;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, 6);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param name0 Property name.
+     * @param val0 Property value.
+     * @param sens0 Property sensitive flag.
+     * @param name1 Property name.
+     * @param val1 Property value.
+     * @param sens1 Property sensitive flag.
+     * @param name2 Property name.
+     * @param val2 Property value.
+     * @param sens2 Property sensitive flag.
+     * @param name3 Property name.
+     * @param val3 Property value.
+     * @param sens3 Property sensitive flag.
+     * @param name4 Property name.
+     * @param val4 Property value.
+     * @param sens4 Property sensitive flag.
+     * @param name5 Property name.
+     * @param val5 Property value.
+     * @param sens5 Property sensitive flag.
+     * @param name6 Property name.
+     * @param val6 Property value.
+     * @param sens6 Property sensitive flag.
+     * @return String presentation.
+     */
+    public static String toString(String str,
+        String name0, @Nullable Object val0, boolean sens0,
+        String name1, @Nullable Object val1, boolean sens1,
+        String name2, @Nullable Object val2, boolean sens2,
+        String name3, @Nullable Object val3, boolean sens3,
+        String name4, @Nullable Object val4, boolean sens4,
+        String name5, @Nullable Object val5, boolean sens5,
+        String name6, @Nullable Object val6, boolean sens6) {
+        assert name0 != null;
+        assert name1 != null;
+        assert name2 != null;
+        assert name3 != null;
+        assert name4 != null;
+        assert name5 != null;
+        assert name6 != null;
+
+        Object[] propNames = new Object[7];
+        Object[] propVals = new Object[7];
+        boolean[] propSens = new boolean[7];
+
+        propNames[0] = name0;
+        propVals[0] = val0;
+        propSens[0] = sens0;
+        propNames[1] = name1;
+        propVals[1] = val1;
+        propSens[1] = sens1;
+        propNames[2] = name2;
+        propVals[2] = val2;
+        propSens[2] = sens2;
+        propNames[3] = name3;
+        propVals[3] = val3;
+        propSens[3] = sens3;
+        propNames[4] = name4;
+        propVals[4] = val4;
+        propSens[4] = sens4;
+        propNames[5] = name5;
+        propVals[5] = val5;
+        propSens[5] = sens5;
+        propNames[6] = name6;
+        propVals[6] = val6;
+        propSens[6] = sens6;
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, 7);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Produces uniformed output of string with context properties
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param triplets Triplets {@code {name, value, sensitivity}}.
+     * @return String presentation.
+     */
+    public static String toString(String str, Object... triplets) {
+        if (triplets.length % 3 != 0)
+            throw new IllegalArgumentException("Array length must be a multiple of 3");
+
+        int propCnt = triplets.length / 3;
+
+        Object[] propNames = new Object[propCnt];
+        Object[] propVals = new Object[propCnt];
+        boolean[] propSens = new boolean[propCnt];
+
+        for (int i = 0; i < propCnt; i++) {
+            Object name = triplets[i * 3];
+
+            assert name != null;
+
+            propNames[i] = name;
+
+            propVals[i] = triplets[i * 3 + 1];
+
+            Object sens = triplets[i * 3 + 2];
+
+            assert sens instanceof Boolean;
+
+            propSens[i] = (Boolean)sens;
+        }
+
+        SBLimitedLength sb = threadLocSB.get();
+
+        boolean newStr = sb.length() == 0;
+
+        try {
+            return toStringImpl(str, sb, propNames, propVals, propSens, propCnt);
+        }
+        finally {
+            if (newStr)
+                sb.reset();
+        }
+    }
+
+    /**
+     * Creates an uniformed string presentation for the binary-like object.
+     *
+     * @param str Output prefix or {@code null} if empty.
+     * @param buf String builder buffer.
+     * @param propNames Names of object properties.
+     * @param propVals Property values.
+     * @param propSens Sensitive flag of values or {@code null} if all values is not sensitive.
+     * @param propCnt Properties count.
+     * @return String presentation of the object.
+     */
+    private static String toStringImpl(String str, SBLimitedLength buf, Object[] propNames, Object[] propVals,
+        boolean[] propSens, int propCnt) {
+
+        boolean newStr = buf.length() == 0;
+
+        if (str != null)
+            buf.a(str).a(" ");
+
+        buf.a("[");
+
+        appendVals(buf, true, propNames, propVals, propSens, propCnt);
+
+        buf.a(']');
+
+        if (newStr)
+            return buf.toString();
+
+        // Called from another ITSB.toString(), so this string is already in the buffer and shouldn't be returned.
+        return "";
+    }
+
+    /**
+     * Append additional values to the buffer.
+     *
+     * @param buf Buffer.
+     * @param first First value flag.
+     * @param addNames Names of additional values to be included.
+     * @param addVals Additional values to be included.
+     * @param addSens Sensitive flag of values or {@code null} if all values are not sensitive.
+     * @param addLen How many additional values will be included.
+     */
+    private static void appendVals(SBLimitedLength buf,
+        boolean first,
+        Object[] addNames,
+        Object[] addVals,
+        boolean[] addSens,
+        int addLen) {
+        if (addLen > 0) {
+            for (int i = 0; i < addLen; i++) {
+                Object addVal = addVals[i];
+
+                if (addVal != null) {
+                    if (addSens != null && addSens[i] && !includeSensitive())
+                        continue;
+
+                    IgniteToStringInclude incAnn = addVal.getClass().getAnnotation(IgniteToStringInclude.class);
+
+                    if (incAnn != null && incAnn.sensitive() && !includeSensitive())
+                        continue;
+                }
+
+                if (!first)
+                    buf.a(", ");
+                else
+                    first = false;
+
+                buf.a(addNames[i]).a('=');
+
+                toString(buf, addVal);
+            }
+        }
+    }
+
+    /**
+     * @param cls Class.
+     * @param <T> Type of the object.
+     * @return Descriptor for the class.
+     * @throws IllegalAccessException If failed.
+     */
+    @SuppressWarnings({"TooBroadScope"})
+    private static <T> ClassDescriptor getClassDescriptor(Class<T> cls) throws IllegalAccessException {
+        assert cls != null;
+
+        String key = cls.getName() + System.identityHashCode(cls.getClassLoader());
+
+        ClassDescriptor cd = classCache.get(key);
+
+        if (cd != null)
+            return cd;
+
+        cd = new ClassDescriptor(cls);
+
+        final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(cls, MethodHandles.lookup());
+
+        for (Field f : cls.getDeclaredFields()) {
+            boolean add = false;
+
+            Class<?> type = f.getType();
+
+            final IgniteToStringInclude incFld = f.getAnnotation(IgniteToStringInclude.class);
+            final IgniteToStringInclude incType = type.getAnnotation(IgniteToStringInclude.class);
+
+            if (incFld != null || incType != null) {
+                // Information is not sensitive when both the field and the field type are not sensitive.
+                // When @IgniteToStringInclude is not present then the flag is false by default for that attribute.
+                final boolean notSens = (incFld == null || !incFld.sensitive()) && (incType == null || !incType.sensitive());
+                add = notSens || includeSensitive();
+            }
+            else if (!f.isAnnotationPresent(IgniteToStringExclude.class) &&
+                !type.isAnnotationPresent(IgniteToStringExclude.class)
+            ) {
+                if (
+                    // Include only private non-static
+                    Modifier.isPrivate(f.getModifiers()) && !Modifier.isStatic(f.getModifiers()) &&
+
+                        // No direct objects & serializable.
+                        Object.class != type &&
+                        Serializable.class != type &&
+                        Externalizable.class != type &&
+
+                        // No arrays.
+                        !type.isArray() &&
+
+                        // Exclude collections, IO, etc.
+                        !EventListener.class.isAssignableFrom(type) &&
+                        !Map.class.isAssignableFrom(type) &&
+                        !Collection.class.isAssignableFrom(type) &&
+                        !InputStream.class.isAssignableFrom(type) &&
+                        !OutputStream.class.isAssignableFrom(type) &&
+                        !Thread.class.isAssignableFrom(type) &&
+                        !Runnable.class.isAssignableFrom(type) &&
+                        !Lock.class.isAssignableFrom(type) &&
+                        !ReadWriteLock.class.isAssignableFrom(type) &&
+                        !Condition.class.isAssignableFrom(type)
+                )
+                    add = true;
+            }
+
+            if (add) {
+                FieldDescriptor fd = new FieldDescriptor(f, lookup.unreflectVarHandle(f));
+
+                // Get order, if any.
+                final IgniteToStringOrder annOrder = f.getAnnotation(IgniteToStringOrder.class);
+                if (annOrder != null)
+                    fd.setOrder(annOrder.value());
+
+                cd.addField(fd);
+            }
+        }
+
+        cd.sortFields();
+
+        classCache.putIfAbsent(key, cd);
+
+        return cd;
+    }
+
+    /**
+     * Returns sorted and compacted string representation of given {@code col}.
+     * Two nearby numbers with difference at most 1 are compacted to one continuous segment.
+     * E.g. collection of [1, 2, 3, 5, 6, 7, 10] will be compacted to [1-3, 5-7, 10].
+     *
+     * @param col Collection of integers.
+     * @return Compacted string representation of given collections.
+     */
+    public static String compact(Collection<Integer> col) {
+        return compact(col, i -> i + 1);
+    }
+
+    /**
+     * Returns sorted and compacted string representation of given {@code col}.
+     * Two nearby numbers are compacted to one continuous segment.
+     * E.g. collection of [1, 2, 3, 5, 6, 7, 10] with
+     * {@code nextValFun = i -> i + 1} will be compacted to [1-3, 5-7, 10].
+     *
+     * @param col Collection of numbers.
+     * @param nextValFun Function to get nearby number.
+     * @return Compacted string representation of given collections.
+     */
+    public static <T extends Number & Comparable<? super T>> String compact(
+        Collection<T> col,
+        Function<T, T> nextValFun
+    ) {
+        assert nonNull(col);
+        assert nonNull(nextValFun);
+
+        if (col.isEmpty())
+            return "[]";
+
+        IgniteStringBuilder sb = new IgniteStringBuilder();
+        sb.a('[');
+
+        List<T> l = new ArrayList<>(col);
+        Collections.sort(l);
+
+        T left = l.get(0), right = left;
+        for (int i = 1; i < l.size(); i++) {
+            T val = l.get(i);
+
+            if (right.compareTo(val) == 0 || nextValFun.apply(right).compareTo(val) == 0) {
+                right = val;
+                continue;
+            }
+
+            if (left.compareTo(right) == 0)
+                sb.a(left);
+            else
+                sb.a(left).a('-').a(right);
+
+            sb.a(',').a(' ');
+
+            left = right = val;
+        }
+
+        if (left.compareTo(right) == 0)
+            sb.a(left);
+        else
+            sb.a(left).a('-').a(right);
+
+        sb.a(']');
+
+        return sb.toString();
+    }
+
+    /**
+     * Checks that object is already saved.
+     * In positive case this method inserts hash to the saved object entry (if needed) and name@hash for current entry.
+     * Further toString operations are not needed for current object.
+     *
+     * @param buf String builder buffer.
+     * @param obj Object.
+     * @param cls Class.
+     * @param svdObjs Map with saved objects to handle recursion.
+     * @return {@code True} if object is already saved and name@hash was added to buffer.
+     * {@code False} if it wasn't saved previously and it should be saved.
+     */
+    private static boolean handleRecursion(
+        SBLimitedLength buf,
+        Object obj,
+        @NotNull Class<?> cls,
+        IdentityHashMap<Object, EntryReference> svdObjs
+    ) {
+        EntryReference ref = svdObjs.get(obj);
+
+        if (ref == null)
+            return false;
+
+        int pos = ref.pos;
+
+        String name = cls.getSimpleName();
+        String hash = identity(obj);
+        String savedName = name + hash;
+        String charsAtPos = buf.impl().substring(pos, pos + savedName.length());
+
+        if (!buf.isOverflowed() && !savedName.equals(charsAtPos)) {
+            if (charsAtPos.startsWith(cls.getSimpleName())) {
+                buf.i(pos + name.length(), hash);
+
+                incValues(svdObjs, obj, hash.length());
+            }
+            else
+                ref.hashNeeded = true;
+        }
+
+        buf.a(savedName);
+
+        return true;
+    }
+
+    /**
+     * Increment positions of already presented objects afterward given object.
+     *
+     * @param svdObjs Map with objects already presented in the buffer.
+     * @param obj Object.
+     * @param hashLen Length of the object's hash.
+     */
+    private static void incValues(IdentityHashMap<Object, EntryReference> svdObjs, Object obj, int hashLen) {
+        int baseline = svdObjs.get(obj).pos;
+
+        for (IdentityHashMap.Entry<Object, EntryReference> entry : svdObjs.entrySet()) {
+            EntryReference ref = entry.getValue();
+
+            int pos = ref.pos;
+
+            if (pos > baseline)
+                ref.pos = pos + hashLen;
+        }
+    }
+
+    /**
+     * Stub.
+     */
+    protected IgniteToStringBuilder() {
+        // No op.
+    }
+
+    /**
+     *
+     */
+    private static class EntryReference {
+        /** Position. */
+        int pos;
+
+        /** First object entry needs hash to be written. */
+        boolean hashNeeded;
+
+        /**
+         * @param pos Position.
+         */
+        private EntryReference(int pos) {
+            this.pos = pos;
+            hashNeeded = false;
+        }
+    }
+}
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringExclude.java
similarity index 57%
copy from modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java
copy to modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringExclude.java
index a7fe166..48348d5 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringExclude.java
@@ -15,27 +15,22 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.schema;
+package org.apache.ignite.internal.tostring;
 
-import org.apache.ignite.schema.IndexColumn;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /**
- * Non-ordered index column.
+ * Class or field annotated with IgniteToStringInclude claims the element <b>must be</b> excluded
+ * from {@code toString()} output.
+ * This annotation is used to override the default exclusion policy.
  */
-class IndexColumnImpl extends AbstractSchemaObject implements IndexColumn {
-    /**
-     * Constructor.
-     *
-     * @param name Column name.
-     */
-    IndexColumnImpl(String name) {
-        super(name);
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return "Column[" +
-            "name='" + name() + '\'' +
-            ']';
-    }
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.TYPE})
+public @interface IgniteToStringExclude {
+    // No-op.
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringInclude.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringInclude.java
new file mode 100644
index 0000000..850ab69
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringInclude.java
@@ -0,0 +1,45 @@
+/*
+ * 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.internal.tostring;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.lang.IgniteSystemProperties;
+
+/**
+ * Class or field annotated with IgniteToStringInclude claims the element <b>should</b> be included
+ * in {@code toString()} output.
+ * This annotation is used to override the default exclusion policy.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.TYPE})
+public @interface IgniteToStringInclude {
+    /**
+     * A flag indicating if sensitive information stored in the field or fields of the class.<br/>
+     * Such information will be included to {@code toString()} output according to
+     * {@link IgniteSystemProperties#IGNITE_SENSITIVE_DATA_LOGGING} policy.
+     *
+     * @return Attribute value.
+     * @see SensitiveDataLoggingPolicy}.
+     */
+    boolean sensitive() default false;
+}
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/AbstractSchemaObject.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringOrder.java
similarity index 53%
copy from modules/schema/src/main/java/org/apache/ignite/internal/schema/AbstractSchemaObject.java
copy to modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringOrder.java
index 9ad2d9f..db91ad1 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/AbstractSchemaObject.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/IgniteToStringOrder.java
@@ -15,36 +15,26 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.schema;
+package org.apache.ignite.internal.tostring;
 
-import org.apache.ignite.schema.SchemaObject;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /**
- * Schema object base class.
+ * Indicates field order in {@code toString()} output if set.
+ * Fields with smaller order value will come earlier the others in {@code toString()} output.
+ * By default, the order is the same as the order of declaration in the class.
+ * If order is not specified the {@link Integer#MAX_VALUE} will be used.
  */
-public abstract class AbstractSchemaObject implements SchemaObject {
-    /** Schema object name. */
-    private final String name;
-
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface IgniteToStringOrder {
     /**
-     * Constructor.
-     *
-     * @param name Schema object name.
+     * Numeric order value.
      */
-    protected AbstractSchemaObject(String name) {
-        this.name = name;
-    }
-
-    /** {@inheritDoc} */
-    @Override public String name() {
-        return name;
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return "SchemaObject[" +
-            "name='" + name + '\'' +
-            "class=" + getClass().getName() +
-            ']';
-    }
+    int value();
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/S.java
similarity index 63%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
copy to modules/core/src/main/java/org/apache/ignite/internal/tostring/S.java
index 28dd677..1ab2322 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/S.java
@@ -14,25 +14,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.ignite.configuration.validation;
 
-/** */
-public class ValidationIssue {
-    /** */
-    private String message;
+package org.apache.ignite.internal.tostring;
 
-    /** */
-    public ValidationIssue(String message) {
-        this.message = message;
-    }
-
-    /** */
-    public String message() {
-        return message;
-    }
-
-    /** */
-    @Override public String toString() {
-        return "ValidationIssue [message=" + message + ']';
-    }
-}
+/**
+ * Defines a shortcut for {@link IgniteToStringBuilder}.
+ *
+ * Since Java doesn't provide type aliases (like Scala, for example) we resort to these types of measures.
+ * Intended for internal use only and meant to provide for more terse code when readability of code is not compromised.
+ */
+@SuppressWarnings({"ExtendsUtilityClass"})
+public final class S extends IgniteToStringBuilder {
+    /* No-op. */
+}
\ No newline at end of file
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/tostring/SBLimitedLength.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/SBLimitedLength.java
new file mode 100644
index 0000000..b09fc2e
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/SBLimitedLength.java
@@ -0,0 +1,287 @@
+/*
+ * 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.internal.tostring;
+
+import java.util.Arrays;
+import org.apache.ignite.lang.IgniteStringBuilder;
+import org.apache.ignite.lang.IgniteSystemProperties;
+import org.jetbrains.annotations.Nullable;
+
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_TO_STRING_MAX_LENGTH;
+
+/**
+ * String builder with limited length.
+ * <p>
+ * Keeps head and tail for long strings that not fit to the limit cutting the middle of the string.
+ */
+class SBLimitedLength extends IgniteStringBuilder {
+    /** Max string length. */
+    private static final int MAX_STR_LEN = IgniteSystemProperties.getInteger(IGNITE_TO_STRING_MAX_LENGTH, 10_000);
+
+    /** Length of tail part of message. */
+    private static final int TAIL_LEN = MAX_STR_LEN / 10 * 2;
+
+    /** Length of head part of message. */
+    private static final int HEAD_LEN = MAX_STR_LEN - TAIL_LEN;
+
+    /** Additional string builder to get tail of message. */
+    private CircularStringBuilder tail;
+
+    /**
+     * @param cap Capacity.
+     */
+    SBLimitedLength(int cap) {
+        super(cap);
+
+        tail = null;
+    }
+
+    /**
+     * Resets buffer.
+     */
+    public void reset() {
+        setLength(0);
+
+        if (tail != null)
+            tail.reset();
+    }
+
+    /**
+     * @return tail string builder.
+     */
+    public @Nullable CircularStringBuilder getTail() {
+        return tail;
+    }
+
+    /**
+     * @return This builder.
+     */
+    private SBLimitedLength onWrite() {
+        if (!isOverflowed())
+            return this;
+
+        if (tail == null)
+            tail = new CircularStringBuilder(TAIL_LEN);
+
+        if (tail.length() == 0) {
+            int newSbLen = Math.min(length(), HEAD_LEN + 1);
+            tail.append(impl().substring(newSbLen));
+
+            setLength(newSbLen);
+        }
+
+        return this;
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(Object obj) {
+        if (isOverflowed()) {
+            tail.append(obj);
+            return this;
+        }
+
+        super.a(obj);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(String str) {
+        if (isOverflowed()) {
+            tail.append(str);
+            return this;
+        }
+
+        super.a(str);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(StringBuffer sb) {
+        if (isOverflowed()) {
+            tail.append(sb);
+            return this;
+        }
+
+        super.a(sb);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(CharSequence s) {
+        if (isOverflowed()) {
+            tail.append(s);
+            return this;
+        }
+
+        super.a(s);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(CharSequence s, int start, int end) {
+        if (isOverflowed()) {
+            tail.append(s.subSequence(start, end));
+            return this;
+        }
+
+        super.a(s, start, end);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(char[] str) {
+        if (isOverflowed()) {
+            tail.append(str);
+            return this;
+        }
+
+        super.a(str);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(char[] str, int offset, int len) {
+        if (isOverflowed()) {
+            tail.append(Arrays.copyOfRange(str, offset, len));
+            return this;
+        }
+
+        super.a(str, offset, len);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @SuppressWarnings("BooleanParameter")
+    @Override public SBLimitedLength a(boolean b) {
+        if (isOverflowed()) {
+            tail.append(b);
+            return this;
+        }
+
+        super.a(b);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(char c) {
+        if (isOverflowed()) {
+            tail.append(c);
+            return this;
+        }
+
+        super.a(c);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(int i) {
+        if (isOverflowed()) {
+            tail.append(i);
+            return this;
+        }
+
+        super.a(i);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(long lng) {
+        if (isOverflowed()) {
+            tail.append(lng);
+            return this;
+        }
+
+        super.a(lng);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(float f) {
+        if (isOverflowed()) {
+            tail.append(f);
+            return this;
+        }
+
+        super.a(f);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength a(double d) {
+        if (isOverflowed()) {
+            tail.append(d);
+            return this;
+        }
+
+        super.a(d);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public SBLimitedLength appendCodePoint(int codePoint) {
+        if (isOverflowed()) {
+            tail.append(codePoint);
+            return this;
+        }
+
+        super.appendCodePoint(codePoint);
+
+        return onWrite();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        if (tail == null)
+            return super.toString();
+        else {
+            int tailLen = tail.length();
+            StringBuilder res = new StringBuilder(impl().capacity() + tailLen + 100);
+
+            res.append(impl());
+
+            if (tail.getSkipped() > 0) {
+                res.append("... and ").append((tail.getSkipped() + tailLen))
+                    .append(" skipped ...");
+            }
+
+            res.append(tail.toString());
+
+            return res.toString();
+        }
+    }
+
+    /**
+     * @return {@code True} - if buffer limit is reached.
+     */
+    public boolean isOverflowed() {
+        return impl().length() > HEAD_LEN;
+    }
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/SensitiveDataLoggingPolicy.java
similarity index 66%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
copy to modules/core/src/main/java/org/apache/ignite/internal/tostring/SensitiveDataLoggingPolicy.java
index 28dd677..1cac58e 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/SensitiveDataLoggingPolicy.java
@@ -14,25 +14,25 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.ignite.configuration.validation;
 
-/** */
-public class ValidationIssue {
-    /** */
-    private String message;
+package org.apache.ignite.internal.tostring;
 
-    /** */
-    public ValidationIssue(String message) {
-        this.message = message;
-    }
+/**
+ * Log levels for sensitive data
+ */
+public enum SensitiveDataLoggingPolicy {
+    /**
+     * Write sensitive information in {@code toString()} output.
+     */
+    PLAIN,
 
-    /** */
-    public String message() {
-        return message;
-    }
+    /**
+     * Write hash of sensitive information in {@code toString()} output.
+     */
+    HASH,
 
-    /** */
-    @Override public String toString() {
-        return "ValidationIssue [message=" + message + ']';
-    }
+    /**
+     * Don't write sensitive information in {@code toString()} output.
+     */
+    NONE;
 }
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java b/modules/core/src/main/java/org/apache/ignite/internal/tostring/package-info.java
similarity index 65%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
copy to modules/core/src/main/java/org/apache/ignite/internal/tostring/package-info.java
index 28dd677..9cc76fb 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/tostring/package-info.java
@@ -14,25 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.ignite.configuration.validation;
 
-/** */
-public class ValidationIssue {
-    /** */
-    private String message;
-
-    /** */
-    public ValidationIssue(String message) {
-        this.message = message;
-    }
-
-    /** */
-    public String message() {
-        return message;
-    }
-
-    /** */
-    @Override public String toString() {
-        return "ValidationIssue [message=" + message + ']';
-    }
-}
+/**
+ * Common utility classes for strings.
+ */
+package org.apache.ignite.internal.tostring;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
index 283f433..58b7e02 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/util/IgniteUtils.java
@@ -113,4 +113,58 @@ public class IgniteUtils {
     public static <K, V> LinkedHashMap<K, V> newLinkedHashMap(int expSize) {
         return new LinkedHashMap<>(capacity(expSize));
     }
+
+    /**
+     * Applies a supplemental hash function to a given hashCode, which
+     * defends against poor quality hash functions.  This is critical
+     * because ConcurrentHashMap uses power-of-two length hash tables,
+     * that otherwise encounter collisions for hashCodes that do not
+     * differ in lower or upper bits.
+     * <p>
+     * This function has been taken from Java 8 ConcurrentHashMap with
+     * slightly modifications.
+     *
+     * @param h Value to hash.
+     * @return Hash value.
+     */
+    public static int hash(int h) {
+        // Spread bits to regularize both segment and index locations,
+        // using variant of single-word Wang/Jenkins hash.
+        h += (h << 15) ^ 0xffffcd7d;
+        h ^= (h >>> 10);
+        h += (h << 3);
+        h ^= (h >>> 6);
+        h += (h << 2) + (h << 14);
+
+        return h ^ (h >>> 16);
+    }
+
+    /**
+     * Applies a supplemental hash function to a given hashCode, which
+     * defends against poor quality hash functions.  This is critical
+     * because ConcurrentHashMap uses power-of-two length hash tables,
+     * that otherwise encounter collisions for hashCodes that do not
+     * differ in lower or upper bits.
+     * <p>
+     * This function has been taken from Java 8 ConcurrentHashMap with
+     * slightly modifications.
+     *
+     * @param obj Value to hash.
+     * @return Hash value.
+     */
+    public static int hash(Object obj) {
+        return hash(obj.hashCode());
+    }
+
+    /**
+     * A primitive override of {@link #hash(Object)} to avoid unnecessary boxing.
+     *
+     * @param key Value to hash.
+     * @return Hash value.
+     */
+    public static int hash(long key) {
+        int val = (int)(key ^ (key >>> 32));
+
+        return hash(val);
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/lang/IgniteStringBuilder.java b/modules/core/src/main/java/org/apache/ignite/lang/IgniteStringBuilder.java
new file mode 100644
index 0000000..d21997b
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/lang/IgniteStringBuilder.java
@@ -0,0 +1,504 @@
+/*
+ * 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.lang;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+/**
+ * Optimized string builder with better API.
+ */
+public class IgniteStringBuilder implements Serializable {
+    /** System line separator. */
+    private static final String NL = System.getProperty("line.separator");
+
+    /** */
+    private static final long serialVersionUID = 0L;
+
+    /** */
+    private StringBuilder impl;
+
+    /**
+     * @see StringBuilder#StringBuilder()
+     */
+    public IgniteStringBuilder() {
+        impl = new StringBuilder(16);
+    }
+
+    /**
+     *
+     * @param cap Initial capacity.
+     * @see StringBuilder#StringBuilder(int)
+     */
+    public IgniteStringBuilder(int cap) {
+        impl = new StringBuilder(cap);
+    }
+
+    /**
+     *
+     * @param str Initial string.
+     * @see StringBuilder#StringBuilder(String)
+     */
+    public IgniteStringBuilder(String str) {
+        impl = new StringBuilder(str);
+    }
+
+    /**
+     * @param seq Initial character sequence.
+     * @see StringBuilder#StringBuilder(CharSequence)
+     */
+    public IgniteStringBuilder(CharSequence seq) {
+        impl = new StringBuilder(seq);
+    }
+
+    /**
+     *
+     * @param len Length to set.
+     */
+    public void setLength(int len) {
+        impl.setLength(len);
+    }
+
+    /**
+     * Gets underlying implementation.
+     *
+     * @return Underlying implementation.
+     */
+    public StringBuilder impl() {
+        assert impl != null;
+
+        return impl;
+    }
+
+    /**
+     *
+     * @return Buffer length.
+     */
+    public int length() {
+        return impl.length();
+    }
+
+    /**
+     *
+     * @param obj Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(Object obj) {
+        impl.append(obj);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param str Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(String str) {
+        impl.append(str);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param sb Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(StringBuffer sb) {
+        impl.append(sb);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param s Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(CharSequence s) {
+        impl.append(s);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param s Element to add.
+     * @param start Start position.
+     * @param end End position.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(CharSequence s, int start, int end) {
+        impl.append(s, start, end);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param str Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(char[] str) {
+        impl.append(str);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param str Element to add.
+     * @param offset Start offset.
+     * @param len Length.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(char[] str, int offset, int len) {
+        impl.append(str, offset, len);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param b Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    @SuppressWarnings("BooleanParameter")
+    public IgniteStringBuilder a(boolean b) {
+        impl.append(b);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param c Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(char c) {
+        impl.append(c);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param i Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(int i) {
+        impl.append(i);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param lng Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(long lng) {
+        impl.append(lng);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param f Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(float f) {
+        impl.append(f);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param d Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder a(double d) {
+        impl.append(d);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param codePoint Element to add.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder appendCodePoint(int codePoint) {
+        impl.appendCodePoint(codePoint);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param start Start position to delete from.
+     * @param end End position.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder d(int start, int end) {
+        impl.delete(start, end);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param index Index to delete character at.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder d(int index) {
+        impl.deleteCharAt(index);
+
+        return this;
+    }
+
+    /**
+     * Adds a platform-dependent newline to this buffer.
+     *
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder nl() {
+        impl.append(NL);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param start Start position to replace from.
+     * @param end End position.
+     * @param str String to replace with.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder r(int start, int end, String str) {
+        impl.replace(start, end, str);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param index Start index to insert to.
+     * @param str String to insert.
+     * @param off Offset in the string to be inserted.
+     * @param len Length of the substring to be inserted.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int index, char[] str, int off, int len) {
+        impl.insert(index, str, off, len);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param off Offset to be inserted at.
+     * @param obj Object whose string representation to be inserted.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, Object obj) {
+        return i(off, String.valueOf(obj));
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param str String to be inserted.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, String str) {
+        impl.insert(off, str);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param str String to be inserted.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, char[] str) {
+        impl.insert(off, str);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param dstOff Offset to insert at.
+     * @param s String to insert.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int dstOff, CharSequence s) {
+        impl.insert(dstOff, s);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param dstOff Offset to insert at.
+     * @param s String to insert.
+     * @param start Start index to insert from.
+     * @param end End index to insert up to.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int dstOff, CharSequence s, int start, int end) {
+        impl.insert(dstOff, s, start, end);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param b Element to insert.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, boolean b) {
+        impl.insert(off, b);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param c Element to insert.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, char c) {
+        impl.insert(off, c);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param i Element to insert.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, int i) {
+        return i(off, String.valueOf(i));
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param l Element to insert.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, long l) {
+        return i(off, String.valueOf(l));
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param f Element to insert.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, float f) {
+        return i(off, String.valueOf(f));
+    }
+
+    /**
+     *
+     * @param off Offset to insert at.
+     * @param d Element to insert.
+     * @return This buffer for chaining method calls.
+     */
+    public IgniteStringBuilder i(int off, double d) {
+        return i(off, String.valueOf(d));
+    }
+
+    /**
+     * Appends given long value as a hex string to this string builder.
+     *
+     * @param val Value to append.
+     * @return This builder for chaining method calls.
+     */
+    public IgniteStringBuilder appendHex(long val) {
+        String hex = Long.toHexString(val);
+
+        int len = hex.length();
+
+        for (int i = 0; i < 16 - len; i++)
+            a('0');
+
+        a(hex);
+
+        return this;
+    }
+
+    /**
+     * Appends given long value as a hex string to this string builder.
+     *
+     * @param val Value to append.
+     * @return This builder for chaining method calls.
+     */
+    public IgniteStringBuilder appendHex(int val) {
+        String hex = Integer.toHexString(val);
+
+        int len = hex.length();
+
+        for (int i = 0; i < 8 - len; i++)
+            a('0');
+
+        a(hex);
+
+        return this;
+    }
+
+    /**
+     *
+     * @param s Stream to write to.
+     * @throws IOException Thrown in case of any IO errors.
+     */
+    private void writeObject(ObjectOutputStream s) throws IOException {
+        s.writeObject(impl);
+    }
+
+    /**
+     *
+     * @param s Stream to read from.
+     * @throws IOException Thrown in case of any IO errors.
+     * @throws ClassNotFoundException Thrown if read class cannot be found.
+     */
+    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
+        impl = (StringBuilder) s.readObject();
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return impl.toString();
+    }
+}
diff --git a/modules/core/src/main/java/org/apache/ignite/lang/IgniteSystemProperties.java b/modules/core/src/main/java/org/apache/ignite/lang/IgniteSystemProperties.java
new file mode 100644
index 0000000..6fae6b3
--- /dev/null
+++ b/modules/core/src/main/java/org/apache/ignite/lang/IgniteSystemProperties.java
@@ -0,0 +1,307 @@
+/*
+ * 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.lang;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.ignite.internal.tostring.IgniteToStringBuilder;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Contains constants for all system properties and environmental variables in Ignite.
+ * These properties and variables can be used to affect the behavior of Ignite.
+ */
+public final class IgniteSystemProperties {
+    /**
+     * Setting to {@code PLAIN} enables writing sensitive information in {@code toString()} output.
+     * Setting to {@code HASH"} enables writing hash of sensitive information in {@code toString()} output.
+     * Setting to {@code NONE} disables writing sensitive information in {@code toString()} output.
+     *
+     * Default: {@code HASH}.
+     * @see IgniteToStringBuilder
+     */
+    public static final String IGNITE_SENSITIVE_DATA_LOGGING = "IGNITE_SENSITIVE_DATA_LOGGING";
+
+    /**
+     * Limit collection (map, array) elements number to output.
+     *
+     * Default: 100
+     * @see IgniteToStringBuilder
+     */
+    public static final String IGNITE_TO_STRING_COLLECTION_LIMIT = "IGNITE_TO_STRING_COLLECTION_LIMIT";
+
+    /**
+     * Boolean flag indicating whether {@link IgniteToStringBuilder} should ignore {@link RuntimeException}
+     * when building string representation of an object and just print information about exception into the log
+     * or rethrow.
+     *
+     * Default: {@code True}.
+     * @see IgniteToStringBuilder
+     */
+    public static final String IGNITE_TO_STRING_IGNORE_RUNTIME_EXCEPTION = "IGNITE_TO_STRING_IGNORE_RUNTIME_EXCEPTION";
+
+    /**
+     * Maximum length for {@code IgniteToStringBuilder.toString(...)} methods result.
+     *
+     * Default: 10_000.
+     * @see IgniteToStringBuilder
+     */
+    public static final String IGNITE_TO_STRING_MAX_LENGTH = "IGNITE_TO_STRING_MAX_LENGTH";
+
+    /**
+     * Enforces singleton.
+     */
+    private IgniteSystemProperties() {
+        // No-op.
+    }
+
+    /**
+     * @param enumCls Enum type.
+     * @param name Name of the system property or environment variable.
+     * @return Enum value or {@code null} if the property is not set.
+     */
+    public static <E extends Enum<E>> E getEnum(Class<E> enumCls, String name) {
+        return getEnum(enumCls, name, null);
+    }
+
+    /**
+     * @param name Name of the system property or environment variable.
+     * @return Enum value or the given default.
+     */
+    public static <E extends Enum<E>> E getEnum(String name, E dflt) {
+        return getEnum(dflt.getDeclaringClass(), name, dflt);
+    }
+
+    /**
+     * @param enumCls Enum type.
+     * @param name Name of the system property or environment variable.
+     * @param dflt Default value.
+     * @return Enum value or the given default.
+     */
+    private static <E extends Enum<E>> E getEnum(Class<E> enumCls, String name, E dflt) {
+        assert enumCls != null;
+
+        String val = getString(name);
+
+        if (val == null)
+            return dflt;
+
+        try {
+            return Enum.valueOf(enumCls, val);
+        }
+        catch (IllegalArgumentException ignore) {
+            return dflt;
+        }
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     *
+     * @param name Name of the system property or environment variable.
+     * @return Value of the system property or environment variable.
+     *         Returns {@code null} if neither can be found for given name.
+     */
+    @Nullable public static String getString(String name) {
+        assert name != null;
+
+        String v = System.getProperty(name);
+
+        if (v == null)
+            v = System.getenv(name);
+
+        return v;
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     *
+     * @param name Name of the system property or environment variable.
+     * @param dflt Default value.
+     * @return Value of the system property or environment variable.
+     *         Returns {@code null} if neither can be found for given name.
+     */
+    @Nullable public static String getString(String name, String dflt) {
+        String val = getString(name);
+
+        return val == null ? dflt : val;
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     * The result is transformed to {@code boolean} using {@code Boolean.valueOf()} method.
+     *
+     * @param name Name of the system property or environment variable.
+     * @return Boolean value of the system property or environment variable.
+     *         Returns {@code False} in case neither system property
+     *         nor environment variable with given name is found.
+     */
+    public static boolean getBoolean(String name) {
+        return getBoolean(name, false);
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     * The result is transformed to {@code boolean} using {@code Boolean.valueOf()} method.
+     *
+     * @param name Name of the system property or environment variable.
+     * @param dflt Default value.
+     * @return Boolean value of the system property or environment variable.
+     *         Returns default value in case neither system property
+     *         nor environment variable with given name is found.
+     */
+    public static boolean getBoolean(String name, boolean dflt) {
+        String val = getString(name);
+
+        return val == null ? dflt : Boolean.parseBoolean(val);
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     * The result is transformed to {@code int} using {@code Integer.parseInt()} method.
+     *
+     * @param name Name of the system property or environment variable.
+     * @param dflt Default value.
+     * @return Integer value of the system property or environment variable.
+     *         Returns default value in case neither system property
+     *         nor environment variable with given name is found.
+     */
+    public static int getInteger(String name, int dflt) {
+        String s = getString(name);
+
+        if (s == null)
+            return dflt;
+
+        int res;
+
+        try {
+            res = Integer.parseInt(s);
+        }
+        catch (NumberFormatException ignore) {
+            res = dflt;
+        }
+
+        return res;
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     * The result is transformed to {@code float} using {@code Float.parseFloat()} method.
+     *
+     * @param name Name of the system property or environment variable.
+     * @param dflt Default value.
+     * @return Float value of the system property or environment variable.
+     *         Returns default value in case neither system property
+     *         nor environment variable with given name is found.
+     */
+    public static float getFloat(String name, float dflt) {
+        String s = getString(name);
+
+        if (s == null)
+            return dflt;
+
+        float res;
+
+        try {
+            res = Float.parseFloat(s);
+        }
+        catch (NumberFormatException ignore) {
+            res = dflt;
+        }
+
+        return res;
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     * The result is transformed to {@code long} using {@code Long.parseLong()} method.
+     *
+     * @param name Name of the system property or environment variable.
+     * @param dflt Default value.
+     * @return Integer value of the system property or environment variable.
+     *         Returns default value in case neither system property
+     *         nor environment variable with given name is found.
+     */
+    public static long getLong(String name, long dflt) {
+        String s = getString(name);
+
+        if (s == null)
+            return dflt;
+
+        long res;
+
+        try {
+            res = Long.parseLong(s);
+        }
+        catch (NumberFormatException ignore) {
+            res = dflt;
+        }
+
+        return res;
+    }
+
+    /**
+     * Gets either system property or environment variable with given name.
+     * The result is transformed to {@code double} using {@code Double.parseDouble()} method.
+     *
+     * @param name Name of the system property or environment variable.
+     * @param dflt Default value.
+     * @return Integer value of the system property or environment variable.
+     *         Returns default value in case neither system property
+     *         nor environment variable with given name is found.
+     */
+    public static double getDouble(String name, double dflt) {
+        String s = getString(name);
+
+        if (s == null)
+            return dflt;
+
+        double res;
+
+        try {
+            res = Double.parseDouble(s);
+        }
+        catch (NumberFormatException ignore) {
+            res = dflt;
+        }
+
+        return res;
+    }
+
+    /**
+     * Gets snapshot of system properties.
+     * Snapshot could be used for thread safe iteration over system properties.
+     * Non-string properties are removed before return.
+     *
+     * @return Snapshot of system properties.
+     */
+    public static Properties snapshot() {
+        Properties sysProps = (Properties)System.getProperties().clone();
+
+        Iterator<Map.Entry<Object, Object>> iter = sysProps.entrySet().iterator();
+
+        while (iter.hasNext()) {
+            Map.Entry entry = iter.next();
+
+            if (!(entry.getValue() instanceof String) || !(entry.getKey() instanceof String))
+                iter.remove();
+        }
+
+        return sysProps;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/testframework/IgniteAbstractTest.java b/modules/core/src/test/java/org/apache/ignite/internal/testframework/IgniteAbstractTest.java
new file mode 100644
index 0000000..50c10de
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/testframework/IgniteAbstractTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.internal.testframework;
+
+import org.apache.ignite.internal.tostring.S;
+import org.apache.ignite.internal.tostring.SensitiveDataLoggingPolicy;
+import org.apache.ignite.lang.IgniteLogger;
+
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_SENSITIVE_DATA_LOGGING;
+import static org.apache.ignite.lang.IgniteSystemProperties.getString;
+
+/**
+ * Ignite base test class.
+ */
+public abstract class IgniteAbstractTest {
+    /** Logger. */
+    protected static IgniteLogger log;
+
+    /** Init test env. */
+    static {
+        S.setSensitiveDataLoggingPolicySupplier(() ->
+            SensitiveDataLoggingPolicy.valueOf(getString(IGNITE_SENSITIVE_DATA_LOGGING, "hash").toUpperCase()));
+    }
+
+    /**
+     * Constructor.
+     */
+    @SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
+    protected IgniteAbstractTest() {
+        log = IgniteLogger.forClass(getClass());
+    }
+
+    /**
+     * @return Logger.
+     */
+    protected IgniteLogger logger() {
+        return log;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/testframework/SystemPropertiesExtension.java b/modules/core/src/test/java/org/apache/ignite/internal/testframework/SystemPropertiesExtension.java
new file mode 100644
index 0000000..a1c9caa
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/testframework/SystemPropertiesExtension.java
@@ -0,0 +1,198 @@
+/*
+ * 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.internal.testframework;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+/**
+ * JUnit rule that manages usage of {@link WithSystemProperty} annotations.<br/>
+ * Should be used in {@link ExtendWith}.
+ *
+ * @see WithSystemProperty
+ * @see ExtendWith
+ */
+public class SystemPropertiesExtension implements
+    BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback {
+    /** Class properties. */
+    @SuppressWarnings("InstanceVariableMayNotBeInitialized")
+    private List<Prop<String, String>> testMethodSysProps;
+
+    /** Class properties. */
+    @SuppressWarnings("InstanceVariableMayNotBeInitialized")
+    private List<Prop<String, String>> testClassSysProps;
+
+    /** {@inheritDoc} */
+    @Override public void beforeAll(ExtensionContext ctx) {
+        testClassSysProps = extractSystemPropertiesBeforeClass(ctx.getRequiredTestClass());
+    }
+
+    /** {@inheritDoc} */
+    @Override public void afterAll(ExtensionContext context) {
+        clearSystemProperties(testClassSysProps);
+
+        testClassSysProps = null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void beforeEach(ExtensionContext ctx) {
+        testMethodSysProps = extractSystemPropertiesBeforeTestMethod(ctx.getRequiredTestMethod());
+    }
+
+    /** {@inheritDoc} */
+    @Override public void afterEach(ExtensionContext context) {
+        clearSystemProperties(testMethodSysProps);
+
+        testMethodSysProps = null;
+    }
+
+    /**
+     * Set system properties before class.
+     *
+     * @param testCls Current test class.
+     * @return List of updated properties in reversed order.
+     */
+    private List<Prop<String, String>> extractSystemPropertiesBeforeClass(Class<?> testCls) {
+        List<WithSystemProperty[]> allProps = new ArrayList<>();
+
+        for (Class<?> cls = testCls; cls != null; cls = cls.getSuperclass()) {
+            SystemPropertiesList clsProps = cls.getAnnotation(SystemPropertiesList.class);
+
+            if (clsProps != null)
+                allProps.add(clsProps.value());
+            else {
+                WithSystemProperty clsProp = cls.getAnnotation(WithSystemProperty.class);
+
+                if (clsProp != null)
+                    allProps.add(new WithSystemProperty[] {clsProp});
+            }
+        }
+
+        Collections.reverse(allProps);
+
+        // List of system properties to set when all tests in class are finished.
+        final List<Prop<String, String>> clsSysProps = new ArrayList<>();
+
+        for (WithSystemProperty[] props : allProps) {
+            for (WithSystemProperty prop : props) {
+                String oldVal = System.setProperty(prop.key(), prop.value());
+
+                clsSysProps.add(new Prop<>(prop.key(), oldVal));
+            }
+        }
+
+        Collections.reverse(clsSysProps);
+
+        return clsSysProps;
+    }
+
+    /**
+     * Set system properties before test method.
+     *
+     * @param testMtd Current test method.
+     * @return List of updated properties in reversed order.
+     */
+    public List<Prop<String, String>> extractSystemPropertiesBeforeTestMethod(Method testMtd) {
+        WithSystemProperty[] allProps = null;
+
+        SystemPropertiesList testProps = testMtd.getAnnotation(SystemPropertiesList.class);
+
+        if (testProps != null)
+            allProps = testProps.value();
+        else {
+            WithSystemProperty testProp = testMtd.getAnnotation(WithSystemProperty.class);
+
+            if (testProp != null)
+                allProps = new WithSystemProperty[] {testProp};
+        }
+
+        // List of system properties to set when test is finished.
+        List<Prop<String, String>> testSysProps = new ArrayList<>();
+
+        if (allProps != null) {
+            for (WithSystemProperty prop : allProps) {
+                String oldVal = System.setProperty(prop.key(), prop.value());
+
+                testSysProps.add(new Prop<>(prop.key(), oldVal));
+            }
+        }
+
+        Collections.reverse(testSysProps);
+
+        return testSysProps;
+    }
+
+    /**
+     * Return old values of updated properties.
+     *
+     * @param sysProps List of properties to clear.
+     */
+    private void clearSystemProperties(List<Prop<String, String>> sysProps) {
+        if (sysProps == null)
+            return; // Nothing to do.
+
+        for (Prop<String, String> prop : sysProps) {
+            if (prop.value() == null)
+                System.clearProperty(prop.key());
+            else
+                System.setProperty(prop.key(), prop.value());
+        }
+    }
+
+    /**
+     * Property.
+     */
+    public static class Prop<K, V> {
+        /** Property key. */
+        private final K key;
+
+        /** Property value. */
+        private final V val;
+
+        /**
+         * @param key Property key.
+         * @param val Property value.
+         */
+        Prop(K key, V val) {
+            this.key = key;
+            this.val = val;
+        }
+
+        /**
+         * @return Property key.
+         */
+        K key() {
+            return key;
+        }
+
+        /**
+         * @return Property value.
+         */
+        V value() {
+            return val;
+        }
+    }
+}
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java b/modules/core/src/test/java/org/apache/ignite/internal/testframework/SystemPropertiesList.java
similarity index 57%
copy from modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java
copy to modules/core/src/test/java/org/apache/ignite/internal/testframework/SystemPropertiesList.java
index a7fe166..1b2835b 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/testframework/SystemPropertiesList.java
@@ -15,27 +15,21 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.internal.schema;
+package org.apache.ignite.internal.testframework;
 
-import org.apache.ignite.schema.IndexColumn;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 
 /**
- * Non-ordered index column.
+ * {@link Repeatable} for the {@link WithSystemProperty}.<br/>
+ * Not intended for direct usage. Use multiple {@link WithSystemProperty} annotation instead.
  */
-class IndexColumnImpl extends AbstractSchemaObject implements IndexColumn {
-    /**
-     * Constructor.
-     *
-     * @param name Column name.
-     */
-    IndexColumnImpl(String name) {
-        super(name);
-    }
-
-    /** {@inheritDoc} */
-    @Override public String toString() {
-        return "Column[" +
-            "name='" + name() + '\'' +
-            ']';
-    }
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface SystemPropertiesList {
+    /** Array of underlying annotations. */
+    WithSystemProperty[] value();
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/testframework/WithSystemProperty.java b/modules/core/src/test/java/org/apache/ignite/internal/testframework/WithSystemProperty.java
new file mode 100644
index 0000000..24407ff
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/testframework/WithSystemProperty.java
@@ -0,0 +1,114 @@
+/*
+ * 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.internal.testframework;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+
+/**
+ * Annotation that defines a scope with specific system property configured.<br/>
+ * <br/>
+ * Might be used on class level or on method level. Multiple annotations might be applied to the same class/method.<br/>
+ * <br/>
+ * In short, these two approaches are basically equivalent:<br/>
+ * <br/>
+ * Short:
+ * <pre>{@code  @WithSystemProperty(key = "name", value = "val")
+ *  public class SomeTest {
+ *  }
+ * }</pre>
+ * Long:
+ * <pre>{@code  public class SomeTest {
+ *      private static Object oldVal;
+ *
+ *      @BeforeClass
+ *      public static void beforeClass() {
+ *          oldVal = System.getProperty("name");
+ *
+ *          System.setProperty("name", "val");
+ *      }
+ *
+ *      @AfterClass
+ *      public static void afterClass() {
+ *          if (oldVal == null)
+ *              System.clearProperty("name");
+ *          else
+ *              System.setProperty("name", oldVal);
+ *      }
+ *  }
+ * }</pre>
+ * <p>
+ * Same applies to methods with the difference that annotation translates into something like {@link BeforeEach} and
+ * {@link AfterEach}.
+ * <br/><br/>
+ * <pre>{@code  public class SomeTest {
+ *      @Test
+ *      @WithSystemProperty(key = "name", value = "val")
+ *      public void test() {
+ *          // ...
+ *      }
+ *  }
+ * }</pre>
+ * is equivalent to:
+ * <pre>{@code  public class SomeTest {
+ *      @Test
+ *      public void test() {
+ *          Object oldVal = System.getProperty("name");
+ *
+ *          try {
+ *              // ...
+ *          }
+ *          finally {
+ *              if (oldVal == null)
+ *                  System.clearProperty("name");
+ *              else
+ *                  System.setProperty("name", oldVal);
+ *          }
+ *      }
+ *  }
+ * }</pre>
+ * For class level annotation it applies system properties for the whole class hierarchy (ignoring interfaces, there's
+ * no linearization implemented). More specific classes have higher priority and set their properties last. It all
+ * starts with {@link Object} which, of course, is not annotated.<br/>
+ * <br/>
+ * Test methods do not inherit their annotations from overridden methods of super class.<br/>
+ * <br/>
+ * If more than one annotation is presented on class/method then they will be applied in the same order as they
+ * appear in code. It is achieved with the help of {@link Repeatable} feature of Java annotations -
+ * {@link SystemPropertiesList} is automatically generated in such cases.
+ * For that reason it is not recommended using {@link SystemPropertiesList} directly.
+ *
+ * @see System#setProperty(String, String)
+ * @see SystemPropertiesExtension
+ * @see SystemPropertiesList
+ */
+@Repeatable(SystemPropertiesList.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface WithSystemProperty {
+    /** The name of the system property. */
+    String key();
+
+    /** The value of the system property. */
+    String value();
+}
diff --git a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java b/modules/core/src/test/java/org/apache/ignite/internal/testframework/package-info.java
similarity index 65%
copy from modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
copy to modules/core/src/test/java/org/apache/ignite/internal/testframework/package-info.java
index 28dd677..23ab2d4 100644
--- a/modules/configuration/src/main/java/org/apache/ignite/configuration/validation/ValidationIssue.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/testframework/package-info.java
@@ -14,25 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.ignite.configuration.validation;
 
-/** */
-public class ValidationIssue {
-    /** */
-    private String message;
-
-    /** */
-    public ValidationIssue(String message) {
-        this.message = message;
-    }
-
-    /** */
-    public String message() {
-        return message;
-    }
-
-    /** */
-    @Override public String toString() {
-        return "ValidationIssue [message=" + message + ']';
-    }
-}
+/**
+ * Contains internal tests or test related classes and interfaces.
+ */
+package org.apache.ignite.internal.testframework;
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/tostring/CircularStringBuilderSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/tostring/CircularStringBuilderSelfTest.java
new file mode 100644
index 0000000..1a77252
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/tostring/CircularStringBuilderSelfTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.internal.tostring;
+
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ *
+ */
+public class CircularStringBuilderSelfTest extends IgniteAbstractTest {
+    /**
+     *
+     */
+    @Test
+    public void testCSBPrimitive() {
+        CircularStringBuilder csb = new CircularStringBuilder(1);
+        csb.append((String)null);
+        assertEquals("l", csb.toString());
+        csb.append('1');
+        assertEquals("1", csb.toString());
+
+        CircularStringBuilder csb2 = new CircularStringBuilder(1);
+        csb2.append(1);
+        assertEquals("1", csb2.toString());
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testCSBOverflow() {
+        testSB(3, "1234", 2, "234");
+        testSB(4, "1234", 2, "1234");
+        testSB(5, "1234", 2, "41234");
+        testSB(6, "1234", 2, "341234");
+        testSB(7, "1234", 2, "2341234");
+        testSB(8, "1234", 2, "12341234");
+    }
+
+    /**
+     * @param capacity Capacity.
+     * @param pattern Pattern to add.
+     * @param num How many times pattern should be added.
+     * @param expected Expected string.
+     */
+    private void testSB(int capacity, String pattern, int num, String expected) {
+        CircularStringBuilder csb = new CircularStringBuilder(capacity);
+
+        for (int i = 0; i < num; i++)
+            csb.append(pattern);
+
+        assertEquals(expected, csb.toString());
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/tostring/IgniteToStringBuilderSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/tostring/IgniteToStringBuilderSelfTest.java
new file mode 100644
index 0000000..939edb7
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/tostring/IgniteToStringBuilderSelfTest.java
@@ -0,0 +1,994 @@
+/*
+ * 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.internal.tostring;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReadWriteLock;
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.lang.IgniteInternalException;
+import org.apache.ignite.lang.IgniteSystemProperties;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.ignite.internal.tostring.IgniteToStringBuilder.identity;
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_TO_STRING_COLLECTION_LIMIT;
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_TO_STRING_MAX_LENGTH;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link IgniteToStringBuilder}.
+ */
+public class IgniteToStringBuilderSelfTest extends IgniteAbstractTest {
+    /**
+     *
+     */
+    @Test
+    public void testToString() {
+        TestClass1 obj = new TestClass1();
+
+        assertEquals(obj.toStringManual(), obj.toStringAutomatic());
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToStringWithAdditions() {
+        TestClass1 obj = new TestClass1();
+
+        String manual = obj.toStringWithAdditionalManual();
+
+        String automatic = obj.toStringWithAdditionalAutomatic();
+
+        assertEquals(manual, automatic);
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToStringCheckSimpleListRecursionPrevention() {
+        ArrayList<Object> list1 = new ArrayList<>();
+        ArrayList<Object> list2 = new ArrayList<>();
+
+        list2.add(list1);
+        list1.add(list2);
+
+        assertEquals("ArrayList [size=1]", IgniteToStringBuilder.toString(ArrayList.class, list1));
+        assertEquals("ArrayList [size=1]", IgniteToStringBuilder.toString(ArrayList.class, list2));
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToStringCheckSimpleMapRecursionPrevention() {
+        HashMap<Object, Object> map1 = new HashMap<>();
+        HashMap<Object, Object> map2 = new HashMap<>();
+
+        map1.put("2", map2);
+        map2.put("1", map1);
+
+        assertEquals("HashMap []", IgniteToStringBuilder.toString(HashMap.class, map1));
+        assertEquals("HashMap []", IgniteToStringBuilder.toString(HashMap.class, map2));
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToStringCheckListAdvancedRecursionPrevention() {
+        ArrayList<Object> list1 = new ArrayList<>();
+        ArrayList<Object> list2 = new ArrayList<>();
+
+        list2.add(list1);
+        list1.add(list2);
+
+        final String hash1 = identity(list1);
+        final String hash2 = identity(list2);
+
+        assertEquals("ArrayList" + hash1 + " [size=1, name=ArrayList [ArrayList" + hash1 + "]]",
+            IgniteToStringBuilder.toString(ArrayList.class, list1, "name", list2));
+        assertEquals("ArrayList" + hash2 + " [size=1, name=ArrayList [ArrayList" + hash2 + "]]",
+            IgniteToStringBuilder.toString(ArrayList.class, list2, "name", list1));
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToStringCheckMapAdvancedRecursionPrevention() {
+        HashMap<Object, Object> map1 = new HashMap<>();
+        HashMap<Object, Object> map2 = new HashMap<>();
+
+        map1.put("2", map2);
+        map2.put("1", map1);
+
+        final String hash1 = identity(map1);
+        final String hash2 = identity(map2);
+
+        assertEquals("HashMap" + hash1 + " [name=HashMap {1=HashMap" + hash1 + "}]",
+            IgniteToStringBuilder.toString(HashMap.class, map1, "name", map2));
+        assertEquals("HashMap" + hash2 + " [name=HashMap {2=HashMap" + hash2 + "}]",
+            IgniteToStringBuilder.toString(HashMap.class, map2, "name", map1));
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testToStringCheckObjectRecursionPrevention() throws Exception {
+        Node n1 = new Node();
+        Node n2 = new Node();
+        Node n3 = new Node();
+        Node n4 = new Node();
+
+        n1.name = "n1";
+        n2.name = "n2";
+        n3.name = "n3";
+        n4.name = "n4";
+
+        n1.next = n2;
+        n2.next = n3;
+        n3.next = n4;
+        n4.next = n3;
+
+        n1.nodes = new Node[4];
+        n2.nodes = n1.nodes;
+        n3.nodes = n1.nodes;
+        n4.nodes = n1.nodes;
+
+        n1.nodes[0] = n1;
+        n1.nodes[1] = n2;
+        n1.nodes[2] = n3;
+        n1.nodes[3] = n4;
+
+        String expN1 = n1.toString();
+        String expN2 = n2.toString();
+        String expN3 = n3.toString();
+        String expN4 = n4.toString();
+
+        CyclicBarrier bar = new CyclicBarrier(4);
+        Executor pool = Executors.newFixedThreadPool(4);
+
+        CompletableFuture<String> fut1 = runAsync(new BarrierCallable(bar, n1, expN1), pool);
+        CompletableFuture<String> fut2 = runAsync(new BarrierCallable(bar, n2, expN2), pool);
+        CompletableFuture<String> fut3 = runAsync(new BarrierCallable(bar, n3, expN3), pool);
+        CompletableFuture<String> fut4 = runAsync(new BarrierCallable(bar, n4, expN4), pool);
+
+        fut1.get(3_000, TimeUnit.MILLISECONDS);
+        fut2.get(3_000, TimeUnit.MILLISECONDS);
+        fut3.get(3_000, TimeUnit.MILLISECONDS);
+        fut4.get(3_000, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * @param callable Callable.
+     * @return Completable future.
+     */
+    private static <U> CompletableFuture<U> runAsync(Callable<U> callable, Executor pool) {
+        return CompletableFuture.supplyAsync(() -> {
+            try {
+                return callable.call();
+            }
+            catch (Throwable th) {
+                throw new IgniteInternalException(th);
+            }
+        }, pool);
+    }
+
+    /**
+     * JUnit.
+     */
+    @Test
+    public void testToStringPerformance() {
+        TestClass1 obj = new TestClass1();
+
+        // Warm up.
+        obj.toStringAutomatic();
+
+        long start = System.currentTimeMillis();
+
+        for (int i = 0; i < 100000; i++)
+            obj.toStringManual();
+
+        logger().info("Manual toString() took: " + (System.currentTimeMillis() - start) + "ms");
+
+        start = System.currentTimeMillis();
+
+        for (int i = 0; i < 100000; i++)
+            obj.toStringAutomatic();
+
+        logger().info("Automatic toString() took: " + (System.currentTimeMillis() - start) + "ms");
+    }
+
+    /**
+     * Test array print.
+     *
+     * @param v value to get array class and fill array.
+     * @param limit value of IGNITE_TO_STRING_COLLECTION_LIMIT.
+     */
+    private <T, V> void testArr(V v, int limit) {
+        T[] arrOf = (T[])Array.newInstance(v.getClass(), limit + 1);
+        Arrays.fill(arrOf, v);
+        T[] arr = Arrays.copyOf(arrOf, limit);
+
+        checkArrayOverflow(arrOf, arr, limit);
+    }
+
+    /**
+     * Test array print.
+     */
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public void testArrLimitWithRecursion() {
+        int limit = IgniteSystemProperties.getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, 100);
+
+        ArrayList[] arrOf = new ArrayList[limit + 1];
+        Arrays.fill(arrOf, new ArrayList());
+        ArrayList[] arr = Arrays.copyOf(arrOf, limit);
+
+        arrOf[0].add(arrOf);
+        arr[0].add(arr);
+
+        checkArrayOverflow(arrOf, arr, limit);
+    }
+
+    /**
+     * @param arrOf Array.
+     * @param arr Array copy.
+     * @param limit Array limit.
+     */
+    private void checkArrayOverflow(Object[] arrOf, Object[] arr, int limit) {
+        String arrStr = IgniteToStringBuilder.arrayToString(arr);
+        String arrOfStr = IgniteToStringBuilder.arrayToString(arrOf);
+
+        // Simulate overflow
+        StringBuilder resultSB = new StringBuilder(arrStr);
+        resultSB.deleteCharAt(resultSB.length() - 1);
+        resultSB.append("... and ").append(arrOf.length - limit).append(" more]");
+
+        arrStr = resultSB.toString();
+
+        assertEquals(arrStr, arrOfStr, "Collection limit error in array of type " +
+            arrOf.getClass().getName() + " error, normal arr: <" + arrStr + ">, overflowed arr: <" + arrOfStr + ">");
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToStringCollectionLimits() {
+        int limit = IgniteSystemProperties.getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, 100);
+
+        Object[] vals = new Object[] {
+            Byte.MIN_VALUE, Boolean.TRUE, Short.MIN_VALUE, Integer.MIN_VALUE, Long.MIN_VALUE,
+            Float.MIN_VALUE, Double.MIN_VALUE, Character.MIN_VALUE, new TestClass1()};
+        for (Object val : vals)
+            testArr(val, limit);
+
+        //noinspection ZeroLengthArrayAllocation
+        int[] intArr1 = new int[0];
+
+        assertEquals("[]", IgniteToStringBuilder.arrayToString(intArr1));
+        assertEquals("null", IgniteToStringBuilder.arrayToString(null));
+
+        int[] intArr2 = {1, 2, 3};
+
+        assertEquals("[1, 2, 3]", IgniteToStringBuilder.arrayToString(intArr2));
+
+        Object[] intArr3 = {2, 3, 4};
+
+        assertEquals("[2, 3, 4]", IgniteToStringBuilder.arrayToString(intArr3));
+
+        byte[] byteArr = new byte[1];
+
+        byteArr[0] = 1;
+        assertEquals(Arrays.toString(byteArr), IgniteToStringBuilder.arrayToString(byteArr));
+        byteArr = Arrays.copyOf(byteArr, 101);
+        assertTrue(IgniteToStringBuilder.arrayToString(byteArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        boolean[] boolArr = new boolean[1];
+
+        boolArr[0] = true;
+        assertEquals(Arrays.toString(boolArr), IgniteToStringBuilder.arrayToString(boolArr));
+        boolArr = Arrays.copyOf(boolArr, 101);
+        assertTrue(IgniteToStringBuilder.arrayToString(boolArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        short[] shortArr = new short[1];
+
+        shortArr[0] = 100;
+        assertEquals(Arrays.toString(shortArr), IgniteToStringBuilder.arrayToString(shortArr));
+        shortArr = Arrays.copyOf(shortArr, 101);
+        assertTrue(IgniteToStringBuilder.arrayToString(shortArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        int[] intArr = new int[1];
+
+        intArr[0] = 10000;
+        assertEquals(Arrays.toString(intArr), IgniteToStringBuilder.arrayToString(intArr));
+        intArr = Arrays.copyOf(intArr, 101);
+        assertTrue(IgniteToStringBuilder.arrayToString(intArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        long[] longArr = new long[1];
+
+        longArr[0] = 10000000;
+        assertEquals(Arrays.toString(longArr), IgniteToStringBuilder.arrayToString(longArr));
+        longArr = Arrays.copyOf(longArr, 101);
+        assertTrue(
+            IgniteToStringBuilder.arrayToString(longArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        float[] floatArr = new float[1];
+
+        floatArr[0] = 1.f;
+        assertEquals(Arrays.toString(floatArr), IgniteToStringBuilder.arrayToString(floatArr));
+        floatArr = Arrays.copyOf(floatArr, 101);
+        assertTrue(IgniteToStringBuilder.arrayToString(floatArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        double[] doubleArr = new double[1];
+
+        doubleArr[0] = 1.;
+        assertEquals(Arrays.toString(doubleArr), IgniteToStringBuilder.arrayToString(doubleArr));
+        doubleArr = Arrays.copyOf(doubleArr, 101);
+        assertTrue(IgniteToStringBuilder.arrayToString(doubleArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        char[] cArr = new char[1];
+
+        cArr[0] = 'a';
+        assertEquals(Arrays.toString(cArr), IgniteToStringBuilder.arrayToString(cArr));
+        cArr = Arrays.copyOf(cArr, 101);
+        assertTrue(IgniteToStringBuilder.arrayToString(cArr).contains("... and 1 more"),
+            "Can't find \"... and 1 more\" in overflowed array string!");
+
+        Map<String, String> strMap = new TreeMap<>();
+        List<String> strList = new ArrayList<>(limit + 1);
+
+        TestClass1 testCls = new TestClass1();
+
+        testCls.strMap = strMap;
+        testCls.strListIncl = strList;
+
+        for (int i = 0; i < limit; i++) {
+            strMap.put("k" + i, "v");
+            strList.add("e");
+        }
+
+        checkColAndMap(testCls);
+    }
+
+    /**
+     *
+     */
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public void testToStringColAndMapLimitWithRecursion() {
+        int limit = IgniteSystemProperties.getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, 100);
+        Map strMap = new TreeMap<>();
+        List strList = new ArrayList<>(limit + 1);
+
+        TestClass1 testClass = new TestClass1();
+        testClass.strMap = strMap;
+        testClass.strListIncl = strList;
+
+        Map m = new TreeMap();
+        m.put("m", strMap);
+
+        List l = new ArrayList();
+        l.add(strList);
+
+        strMap.put("k0", m);
+        strList.add(l);
+
+        for (int i = 1; i < limit; i++) {
+            strMap.put("k" + i, "v");
+            strList.add("e");
+        }
+
+        checkColAndMap(testClass);
+    }
+
+    /**
+     * @param testCls Class with collection and map included in toString().
+     */
+    private void checkColAndMap(TestClass1 testCls) {
+        String testClsStr = IgniteToStringBuilder.toString(TestClass1.class, testCls);
+
+        testCls.strMap.put("kz", "v"); // important to add last element in TreeMap here
+        testCls.strListIncl.add("e");
+
+        String testClsStrOf = IgniteToStringBuilder.toString(TestClass1.class, testCls);
+
+        String testClsStrOfR = testClsStrOf.replaceAll("... and 1 more", "");
+
+        assertEquals(testClsStr.length(), testClsStrOfR.length(),
+            "Collection limit error in Map or List, normal: <" + testClsStr + ">, overflowed: <" + testClsStrOf + ">");
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testToStringSizeLimits() {
+        int limit = IgniteSystemProperties.getInteger(IGNITE_TO_STRING_MAX_LENGTH, 10_000);
+        int tailLen = limit / 10 * 2;
+
+        StringBuilder sb = new StringBuilder(limit + 10);
+
+        sb.append("a".repeat(Math.max(0, limit - 100)));
+
+        String actual = IgniteToStringBuilder.toString(TestClass2.class, new TestClass2(sb.toString()));
+        String exp = "TestClass2 [str=" + sb + ", nullArr=null]";
+
+        assertEquals(exp, actual);
+
+        sb.append("b".repeat(110));
+
+        actual = IgniteToStringBuilder.toString(TestClass2.class, new TestClass2(sb.toString()));
+        exp = "TestClass2 [str=" + sb + ", nullArr=null]";
+
+        assertEquals(exp.substring(0, limit - tailLen), actual.substring(0, limit - tailLen));
+        assertEquals(exp.substring(exp.length() - tailLen), actual.substring(actual.length() - tailLen));
+
+        assertTrue(actual.contains("... and"));
+        assertTrue(actual.contains("skipped ..."));
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testHierarchy() {
+        Wrapper w = new Wrapper();
+        Parent p = w.p;
+        String hash = identity(p);
+
+        assertEquals("Wrapper [p=Child [b=0, pb=Parent[] [null], super=Parent [a=0, pa=Parent[] [null]]]]",
+            w.toString());
+
+        p.pa[0] = p;
+
+        assertEquals("Wrapper [p=Child" + hash + " [b=0, pb=Parent[] [null]," +
+            " super=Parent [a=0, pa=Parent[] [Child" + hash + "]]]]", w.toString());
+
+        ((Child)p).pb[0] = p;
+
+        assertEquals("Wrapper [p=Child" + hash + " [b=0, pb=Parent[] [Child" + hash
+            + "], super=Parent [a=0, pa=Parent[] [Child" + hash + "]]]]", w.toString());
+    }
+
+    /**
+     * Verifies that {@link IgniteToStringBuilder} doesn't fail while iterating over concurrently modified collection.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testToStringCheckConcurrentModificationExceptionFromList() throws Exception {
+        ClassWithList classWithList = new ClassWithList();
+
+        CountDownLatch modificationStartedLatch = new CountDownLatch(1);
+        AtomicBoolean finished = new AtomicBoolean(false);
+
+        final CompletableFuture<Void> finishFut = CompletableFuture.runAsync(() -> {
+            List<SlowToStringObject> list = classWithList.list;
+            for (int i = 0; i < 100; i++)
+                list.add(new SlowToStringObject());
+
+            Random rnd = new Random();
+
+            while (!finished.get()) {
+                if (rnd.nextBoolean() && list.size() > 1)
+                    list.remove(list.size() / 2);
+                else
+                    list.add(list.size() / 2, new SlowToStringObject());
+
+                if (modificationStartedLatch.getCount() > 0)
+                    modificationStartedLatch.countDown();
+            }
+        });
+
+        modificationStartedLatch.await();
+
+        String s = null;
+
+        try {
+            s = classWithList.toString();
+        }
+        finally {
+            finished.set(true);
+
+            finishFut.get();
+
+            assertNotNull(s);
+            assertTrue(s.contains("concurrent modification"));
+        }
+    }
+
+    /**
+     * Verifies that {@link IgniteToStringBuilder} doesn't fail while iterating over concurrently modified map.
+     *
+     * @throws Exception If failed.
+     */
+    @Test
+    public void testToStringCheckConcurrentModificationExceptionFromMap() throws Exception {
+        ClassWithMap classWithMap = new ClassWithMap();
+
+        CountDownLatch modificationStartedLatch = new CountDownLatch(1);
+        AtomicBoolean finished = new AtomicBoolean(false);
+
+        CompletableFuture<Void> finishFut = CompletableFuture.runAsync(() -> {
+            Map<Integer, SlowToStringObject> map = classWithMap.map;
+            for (int i = 0; i < 100; i++)
+                map.put(i, new SlowToStringObject());
+
+            Random rnd = new Random();
+
+            while (!finished.get()) {
+                if (rnd.nextBoolean() && map.size() > 1)
+                    map.remove(map.size() / 2);
+                else
+                    map.put(map.size() / 2, new SlowToStringObject());
+
+                if (modificationStartedLatch.getCount() > 0)
+                    modificationStartedLatch.countDown();
+            }
+        });
+
+        modificationStartedLatch.await();
+
+        String s = null;
+
+        try {
+            s = classWithMap.toString();
+        }
+        finally {
+            finished.set(true);
+
+            finishFut.get();
+
+            assertNotNull(s);
+            assertTrue(s.contains("concurrent modification"));
+        }
+    }
+
+    /**
+     * Test verifies that when RuntimeException is thrown from toString method of some class
+     * IgniteToString builder doesn't fail but finishes building toString representation.
+     */
+    @Test
+    public void testRuntimeExceptionCaught() {
+        WrapperForFaultyToStringClass wr = new WrapperForFaultyToStringClass(
+            new ClassWithFaultyToString[] {new ClassWithFaultyToString()});
+
+        String strRep = wr.toString();
+
+        //field before faulty field was written successfully to string representation
+        assertTrue(strRep.contains("id=12345"));
+
+        //message from RuntimeException was written to string representation
+        assertTrue(strRep.contains("toString failed"));
+
+        //field after faulty field was written successfully to string representation
+        assertTrue(strRep.contains("str=str"));
+    }
+
+    /**
+     * Test class.
+     */
+    @SuppressWarnings("InstanceVariableMayNotBeInitialized")
+    private static class Node {
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        String name;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        Node next;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        Node[] nodes;
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return IgniteToStringBuilder.toString(Node.class, this);
+        }
+    }
+
+    /**
+     * Test class.
+     */
+    private static class BarrierCallable implements Callable<String> {
+        /**
+         *
+         */
+        CyclicBarrier bar;
+
+        /**
+         *
+         */
+        Object obj;
+
+        /** Expected value of {@code toString()} method. */
+        String exp;
+
+        /**
+         *
+         */
+        private BarrierCallable(CyclicBarrier bar, Object obj, String exp) {
+            this.bar = bar;
+            this.obj = obj;
+            this.exp = exp;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String call() throws Exception {
+            for (int i = 0; i < 10; i++) {
+                bar.await();
+
+                assertEquals(exp, obj.toString());
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * Class containing another class with faulty toString implementation
+     * to force IgniteToStringBuilder to call faulty toString.
+     */
+    @SuppressWarnings({"FieldMayBeFinal", "unused"})
+    private static class WrapperForFaultyToStringClass {
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private int id = 12345;
+
+        /**
+         *
+         */
+        @SuppressWarnings("unused")
+        @IgniteToStringInclude
+        private ClassWithFaultyToString[] arr;
+
+        /**
+         *
+         */
+        @SuppressWarnings("unused")
+        @IgniteToStringInclude
+        private String str = "str";
+
+        /**
+         *
+         */
+        WrapperForFaultyToStringClass(ClassWithFaultyToString[] arr) {
+            this.arr = arr;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(WrapperForFaultyToStringClass.class, this);
+        }
+    }
+
+    /**
+     * Class throwing a RuntimeException from a {@link ClassWithFaultyToString#toString()} method.
+     */
+    private static class ClassWithFaultyToString {
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            throw new RuntimeException("toString failed");
+        }
+    }
+
+    /**
+     * Test class.
+     */
+    @SuppressWarnings({"InstanceVariableMayNotBeInitialized", "FieldMayBeFinal", "unused", "FieldMayBeStatic"})
+    private static class TestClass1 {
+        /**
+         *
+         */
+        @IgniteToStringOrder(0)
+        private String id = "1234567890";
+
+        /**
+         *
+         */
+        private int intVar;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude(sensitive = true)
+        private long longVar;
+
+        /**
+         *
+         */
+        @IgniteToStringOrder(1)
+        private UUID uuidVar = UUID.randomUUID();
+
+        /**
+         *
+         */
+        private boolean boolVar;
+
+        /**
+         *
+         */
+        private byte byteVar;
+
+        /**
+         *
+         */
+        private String name = "qwertyuiopasdfghjklzxcvbnm";
+
+        /**
+         *
+         */
+        private final Integer finalInt = 2;
+
+        /**
+         *
+         */
+        private List<String> strList;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private Map<String, String> strMap;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private List<String> strListIncl;
+
+        /**
+         *
+         */
+        private Object obj = new Object();
+
+        /**
+         *
+         */
+        private ReadWriteLock lock;
+
+        /**
+         * @return Manual string.
+         */
+        String toStringManual() {
+            StringBuilder buf = new StringBuilder();
+
+            buf.append(getClass().getSimpleName()).append(" [");
+
+            buf.append("id=").append(id).append(", ");
+            buf.append("uuidVar=").append(uuidVar).append(", ");
+            buf.append("intVar=").append(intVar).append(", ");
+            if (S.includeSensitive())
+                buf.append("longVar=").append(longVar).append(", ");
+            buf.append("boolVar=").append(boolVar).append(", ");
+            buf.append("byteVar=").append(byteVar).append(", ");
+            buf.append("name=").append(name).append(", ");
+            buf.append("finalInt=").append(finalInt).append(", ");
+            buf.append("strMap=").append(strMap).append(", ");
+            buf.append("strListIncl=").append(strListIncl);
+
+            buf.append("]");
+
+            return buf.toString();
+        }
+
+        /**
+         * @return Automatic string.
+         */
+        String toStringAutomatic() {
+            return S.toString(TestClass1.class, this);
+        }
+
+        /**
+         * @return Automatic string with additional parameters.
+         */
+        String toStringWithAdditionalAutomatic() {
+            return S.toString(TestClass1.class, this, "newParam1", 1, false, "newParam2", 2, true);
+        }
+
+        /**
+         * @return Manual string with additional parameters.
+         */
+        String toStringWithAdditionalManual() {
+            StringBuilder s = new StringBuilder(toStringManual());
+            s.setLength(s.length() - 1);
+            s.append(", newParam1=").append(1);
+            if (S.includeSensitive())
+                s.append(", newParam2=").append(2);
+            s.append(']');
+            return s.toString();
+        }
+    }
+
+    /**
+     *
+     */
+    @SuppressWarnings({"InstanceVariableMayNotBeInitialized", "FieldMayBeFinal", "unused"})
+    private static class TestClass2 {
+        /**
+         *
+         */
+        @SuppressWarnings("unused")
+        @IgniteToStringInclude
+        private String str;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private Object[] nullArr;
+
+        /**
+         * @param str String.
+         */
+        TestClass2(String str) {
+            this.str = str;
+        }
+    }
+
+    /**
+     *
+     */
+    @SuppressWarnings("FieldMayBeFinal")
+    private static class ClassWithList {
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private List<SlowToStringObject> list = new LinkedList<>();
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(ClassWithList.class, this);
+        }
+    }
+
+    /**
+     *
+     */
+    @SuppressWarnings("FieldMayBeFinal")
+    private static class ClassWithMap {
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private Map<Integer, SlowToStringObject> map = new HashMap<>();
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(ClassWithMap.class, this);
+        }
+    }
+
+    /**
+     * Class sleeps a short quanta of time to increase chances of data race
+     * between {@link IgniteToStringBuilder} iterating over collection  user thread concurrently modifying it.
+     */
+    private static class SlowToStringObject {
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            try {
+                Thread.sleep(1);
+            }
+            catch (InterruptedException e) {
+                log.error(e.getMessage(), e);
+            }
+
+            return super.toString();
+        }
+    }
+
+    /**
+     *
+     */
+    @SuppressWarnings({"InstanceVariableMayNotBeInitialized", "MismatchedReadAndWriteOfArray", "unused", "FieldMayBeFinal"})
+    private static class Parent {
+        /**
+         *
+         */
+        private int a;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private Parent[] pa = new Parent[1];
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(Parent.class, this);
+        }
+    }
+
+    /**
+     *
+     */
+    @SuppressWarnings({"InstanceVariableMayNotBeInitialized", "MismatchedReadAndWriteOfArray", "unused", "FieldMayBeFinal"})
+    private static class Child extends Parent {
+        /**
+         *
+         */
+        private int b;
+
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        private Parent[] pb = new Parent[1];
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(Child.class, this, super.toString());
+        }
+    }
+
+    /**
+     *
+     */
+    private static class Wrapper {
+        /**
+         *
+         */
+        @IgniteToStringInclude
+        Parent p = new Child();
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(Wrapper.class, this);
+        }
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/tostring/SensitiveDataToStringTest.java b/modules/core/src/test/java/org/apache/ignite/internal/tostring/SensitiveDataToStringTest.java
new file mode 100644
index 0000000..9017b73
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/tostring/SensitiveDataToStringTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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.internal.tostring;
+
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import org.apache.ignite.internal.testframework.IgniteAbstractTest;
+import org.apache.ignite.internal.testframework.SystemPropertiesExtension;
+import org.apache.ignite.internal.testframework.WithSystemProperty;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.apache.ignite.lang.IgniteSystemProperties;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.apache.ignite.internal.tostring.SensitiveDataLoggingPolicy.HASH;
+import static org.apache.ignite.internal.tostring.SensitiveDataLoggingPolicy.NONE;
+import static org.apache.ignite.internal.tostring.SensitiveDataLoggingPolicy.PLAIN;
+import static org.apache.ignite.lang.IgniteSystemProperties.IGNITE_SENSITIVE_DATA_LOGGING;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for output of {@code toString()} depending on the value of
+ * {@link IgniteSystemProperties#IGNITE_SENSITIVE_DATA_LOGGING}
+ */
+@ExtendWith(SystemPropertiesExtension.class)
+public class SensitiveDataToStringTest extends IgniteAbstractTest {
+    /** Random int. */
+    private static final int rndInt0 = 54321;
+
+    /** Random string. */
+    private static final String rndString = "qwer";
+
+    /**
+     *
+     */
+    @Test
+    public void testSensitivePropertiesResolving0() {
+        assertSame(HASH, S.getSensitiveDataLogging(), S.getSensitiveDataLogging().toString());
+    }
+
+    /**
+     *
+     */
+    @Test
+    @WithSystemProperty(key = IGNITE_SENSITIVE_DATA_LOGGING, value = "plain")
+    public void testSensitivePropertiesResolving1() {
+        assertSame(PLAIN, S.getSensitiveDataLogging(), S.getSensitiveDataLogging().toString());
+    }
+
+    /**
+     *
+     */
+    @Test
+    @WithSystemProperty(key = IGNITE_SENSITIVE_DATA_LOGGING, value = "hash")
+    public void testSensitivePropertiesResolving2() {
+        assertSame(HASH, S.getSensitiveDataLogging(), S.getSensitiveDataLogging().toString());
+    }
+
+    /**
+     *
+     */
+    @Test
+    @WithSystemProperty(key = IGNITE_SENSITIVE_DATA_LOGGING, value = "none")
+    public void testSensitivePropertiesResolving3() {
+        assertSame(NONE, S.getSensitiveDataLogging(), S.getSensitiveDataLogging().toString());
+    }
+
+    /**
+     *
+     */
+    @Test
+    @WithSystemProperty(key = IGNITE_SENSITIVE_DATA_LOGGING, value = "plain")
+    public void testTableObjectImplWithSensitive() {
+        testTableObjectImpl((strToCheck, object) -> assertTrue(strToCheck.contains(object.toString()), strToCheck));
+    }
+
+    /**
+     *
+     */
+    @Test
+    @WithSystemProperty(key = IGNITE_SENSITIVE_DATA_LOGGING, value = "hash")
+    public void testTableObjectImplWithHashSensitive() {
+        testTableObjectImpl((strToCheck, object) -> assertTrue(strToCheck.contains(object.toString()), strToCheck));
+    }
+
+    /**
+     *
+     */
+    @Test
+    @WithSystemProperty(key = IGNITE_SENSITIVE_DATA_LOGGING, value = "none")
+    public void testTableObjectImplWithoutSensitive() {
+        testTableObjectImpl((strToCheck, object) -> assertEquals("TableObject", object.toString(), strToCheck));
+    }
+
+    /**
+     *
+     */
+    private void testTableObjectImpl(BiConsumer<String, Object> checker) {
+        Person person = new Person(rndInt0, rndString);
+
+        TableObject testObject = new TableObject(person);
+        checker.accept(testObject.toString(), testObject);
+    }
+
+    /**
+     *
+     */
+    static class TableObject {
+        @IgniteToStringInclude(sensitive = true)
+        Person person;
+
+        TableObject(Person person) {
+            this.person = person;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            switch (S.getSensitiveDataLogging()) {
+                case PLAIN:
+                    return S.toString(getClass().getSimpleName(), "person", person, false);
+
+                case HASH:
+                    return String.valueOf(person == null ? "null" : IgniteUtils.hash(person));
+
+                case NONE:
+                default:
+                    return "TableObject";
+            }
+        }
+    }
+
+    /**
+     *
+     */
+    static class Person {
+        /** Id organization. */
+        int orgId;
+
+        /** Person name. */
+        String name;
+
+        /**
+         * Constructor.
+         *
+         * @param orgId Id organization.
+         * @param name Person name.
+         */
+        Person(int orgId, String name) {
+            this.orgId = orgId;
+            this.name = name;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int hashCode() {
+            return Objects.hash(orgId, name);
+        }
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return S.toString(Person.class, this);
+        }
+    }
+}
diff --git a/modules/network/pom.xml b/modules/network/pom.xml
index 93547f1..3de7c6d 100644
--- a/modules/network/pom.xml
+++ b/modules/network/pom.xml
@@ -44,6 +44,12 @@
 
         <dependency>
             <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-configuration-annotation-processor</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
             <artifactId>ignite-core</artifactId>
             <version>${project.version}</version>
         </dependency>
@@ -66,13 +72,6 @@
             <artifactId>junit-jupiter-engine</artifactId>
             <scope>test</scope>
         </dependency>
-
-        <dependency>
-            <groupId>org.apache.ignite</groupId>
-            <artifactId>ignite-configuration-annotation-processor</artifactId>
-            <version>${project.version}</version>
-            <scope>compile</scope>
-        </dependency>
     </dependencies>
     <build>
         <resources>
diff --git a/modules/network/src/integrationTest/java/org/apache/ignite/network/TestMessage.java b/modules/network/src/integrationTest/java/org/apache/ignite/network/TestMessage.java
index d87715e..3ee3594 100644
--- a/modules/network/src/integrationTest/java/org/apache/ignite/network/TestMessage.java
+++ b/modules/network/src/integrationTest/java/org/apache/ignite/network/TestMessage.java
@@ -21,6 +21,8 @@ package org.apache.ignite.network;
 import java.io.Serializable;
 import java.util.Map;
 import java.util.Objects;
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.network.message.NetworkMessage;
 
 /** */
@@ -31,6 +33,8 @@ public class TestMessage implements NetworkMessage, Serializable {
     /** */
     private final String msg;
 
+    /** */
+    @IgniteToStringInclude
     private final Map<Integer, String> map;
 
     /** */
@@ -48,14 +52,6 @@ public class TestMessage implements NetworkMessage, Serializable {
     }
 
     /** {@inheritDoc} */
-    @Override public String toString() {
-        return "TestMessage{" +
-            "msg='" + msg + '\'' +
-            ", map=" + map +
-            '}';
-    }
-
-    /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
@@ -72,4 +68,9 @@ public class TestMessage implements NetworkMessage, Serializable {
     @Override public short directType() {
         return TYPE;
     }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(TestMessage.class, this);
+    }
 }
diff --git a/modules/network/src/main/java/org/apache/ignite/network/ClusterNode.java b/modules/network/src/main/java/org/apache/ignite/network/ClusterNode.java
index e0ad6a7..205c32f 100644
--- a/modules/network/src/main/java/org/apache/ignite/network/ClusterNode.java
+++ b/modules/network/src/main/java/org/apache/ignite/network/ClusterNode.java
@@ -18,6 +18,7 @@ package org.apache.ignite.network;
 
 import java.io.Serializable;
 import java.util.Objects;
+import org.apache.ignite.internal.tostring.S;
 
 /**
  * Representation of a node in a cluster.
@@ -79,10 +80,6 @@ public class ClusterNode implements Serializable {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "ClusterNode{" +
-            "name='" + name + '\'' +
-            ", host='" + host + '\'' +
-            ", port=" + port +
-            '}';
+        return S.toString(ClusterNode.class, this);
     }
 }
diff --git a/modules/raft-client/src/main/java/org/apache/ignite/raft/client/Peer.java b/modules/raft-client/src/main/java/org/apache/ignite/raft/client/Peer.java
index 6bb66f2..53da9a3 100644
--- a/modules/raft-client/src/main/java/org/apache/ignite/raft/client/Peer.java
+++ b/modules/raft-client/src/main/java/org/apache/ignite/raft/client/Peer.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.raft.client;
 
 import java.io.Serializable;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.network.ClusterNode;
 
 /**
@@ -73,6 +74,7 @@ public final class Peer implements Serializable {
         return priority;
     }
 
+    /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
@@ -85,13 +87,15 @@ public final class Peer implements Serializable {
         return true;
     }
 
+    /** {@inheritDoc} */
     @Override public int hashCode() {
         int result = node.hashCode();
         result = 31 * result + priority;
         return result;
     }
 
+    /** {@inheritDoc} */
     @Override public String toString() {
-        return node.name() + ":" + priority;
+        return S.toString(Peer.class, this);
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/AbstractSchemaObject.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/AbstractSchemaObject.java
index 9ad2d9f..2c1ecbb 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/AbstractSchemaObject.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/AbstractSchemaObject.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.SchemaObject;
 
 /**
@@ -42,9 +43,8 @@ public abstract class AbstractSchemaObject implements SchemaObject {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "SchemaObject[" +
-            "name='" + name + '\'' +
-            "class=" + getClass().getName() +
-            ']';
+        return S.toString("SchemaObject",
+            "name", name,
+            "class", getClass().getName());
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Bitmask.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Bitmask.java
index c3b1eed..172b195 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Bitmask.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Bitmask.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.S;
+
 /**
  * A fixed-sized type representing a bitmask of <code>n</code> bits. The actual size of a bitmask will round up
  * to the smallest number of bytes required to store <code>n</code> bits.
@@ -84,4 +86,9 @@ public class Bitmask extends NativeType {
         else
             return res;
     }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(Bitmask.class.getSimpleName(), "bits", bits, "typeSpec", spec(), "len", length());
+    }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java
index 1758f77..12eaa9c 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Column.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.S;
+
 /**
  * Column description for a type schema. Column contains a column name, a column type and a nullability flag.
  * <p>
@@ -132,6 +134,6 @@ public class Column implements Comparable<Column> {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "Column [idx=" + schemaIndex + ", name=" + name + ", type=" + type + ", nullable=" + nullable + ']';
+        return S.toString(Column.class, this);
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/ColumnImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/ColumnImpl.java
index 24dfc4b..84e75f0 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/ColumnImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/ColumnImpl.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.Column;
 import org.apache.ignite.schema.ColumnType;
 import org.jetbrains.annotations.Nullable;
@@ -32,6 +34,7 @@ public class ColumnImpl extends AbstractSchemaObject implements Column {
     private final boolean nullable;
 
     /** Default value. */
+    @IgniteToStringInclude(sensitive = true)
     private final Object defVal;
 
     /**
@@ -66,11 +69,6 @@ public class ColumnImpl extends AbstractSchemaObject implements Column {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "ColumnImpl[" +
-            "name='" + name() + '\'' +
-            ", type=" + type +
-            ", nullable=" + nullable +
-            ", default=" + defVal +
-            ']';
+        return S.toString(ColumnImpl.class, this);
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Columns.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Columns.java
index 669ed6d..9f7190a 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/Columns.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/Columns.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.schema;
 
 import java.util.Arrays;
 import java.util.NoSuchElementException;
+import org.apache.ignite.internal.tostring.S;
 
 /**
  * A set of columns representing a key or a value chunk in a row.
@@ -283,4 +284,9 @@ public class Columns {
 
         throw new NoSuchElementException("No field '" + fieldName + "' defined");
     }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+       return S.arrayToString(cols);
+    }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/HashIndexImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/HashIndexImpl.java
index cbac7bd..fde1522 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/HashIndexImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/HashIndexImpl.java
@@ -20,6 +20,8 @@ package org.apache.ignite.internal.schema;
 import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Collectors;
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.HashIndex;
 import org.apache.ignite.schema.IndexColumn;
 
@@ -28,6 +30,7 @@ import org.apache.ignite.schema.IndexColumn;
  */
 public class HashIndexImpl extends AbstractSchemaObject implements HashIndex {
     /** Index columns. */
+    @IgniteToStringInclude
     private final List<IndexColumn> columns;
 
     /**
@@ -50,11 +53,9 @@ public class HashIndexImpl extends AbstractSchemaObject implements HashIndex {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "TableIndex[" +
-            "type=HASH, " +
-            "name='" + name() + '\'' +
-            ", columns=[" + columns.stream().map(IndexColumn::name).collect(Collectors.joining()) + ']' +
-            ']';
+        return S.toString(HashIndexImpl.class, this,
+            "type", type(),
+            "name", name());
     }
 
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java
index a7fe166..9948859 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/IndexColumnImpl.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.IndexColumn;
 
 /**
@@ -34,8 +35,6 @@ class IndexColumnImpl extends AbstractSchemaObject implements IndexColumn {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "Column[" +
-            "name='" + name() + '\'' +
-            ']';
+        return S.toString(IndexColumnImpl.class.getSimpleName(), "name", name());
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeType.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeType.java
index 6572a36..d580137 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeType.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeType.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.S;
+
 /**
  * A thin wrapper over {@link NativeTypeSpec} to instantiate parameterized constrained types.
  */
@@ -55,6 +57,7 @@ public class NativeType implements Comparable<NativeType> {
     private final int len;
 
     /**
+     *
      */
     protected NativeType(NativeTypeSpec typeSpec, int len) {
         if (!typeSpec.fixedLength())
@@ -68,6 +71,7 @@ public class NativeType implements Comparable<NativeType> {
     }
 
     /**
+     *
      */
     protected NativeType(NativeTypeSpec typeSpec) {
         if (typeSpec.fixedLength())
@@ -137,6 +141,9 @@ public class NativeType implements Comparable<NativeType> {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "NativeType [typeSpec=" + typeSpec + ", len=" + len + ']';
+        return S.toString(NativeType.class.getSimpleName(),
+            "name", typeSpec.name(),
+            "len", len,
+            "fixed", typeSpec.fixedLength());
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeTypeSpec.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeTypeSpec.java
index a5ae386..cc470aa 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeTypeSpec.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/NativeTypeSpec.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.S;
+
 /**
  * Base class for storage built-in data types definition. The class contains predefined values
  * for fixed-sized types and some of the variable-sized types. Parameterized types, such as
@@ -173,6 +175,8 @@ public enum NativeTypeSpec {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "NativeTypeSpec [desc=" + desc + ", fixedSize=" + fixedSize + ']';
+        return S.toString(NativeTypeSpec.class.getSimpleName(),
+            "name", name(),
+            "fixed", fixedLength());
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/PartialIndexImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/PartialIndexImpl.java
index 09c2295..3500497 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/PartialIndexImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/PartialIndexImpl.java
@@ -18,8 +18,7 @@
 package org.apache.ignite.internal.schema;
 
 import java.util.List;
-import java.util.stream.Collectors;
-import org.apache.ignite.schema.IndexColumn;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.PartialIndex;
 import org.apache.ignite.schema.SortedIndexColumn;
 
@@ -51,11 +50,10 @@ public class PartialIndexImpl extends SortedIndexImpl implements PartialIndex {
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "PartialIndex[" +
-            "name='" + name() + '\'' +
-            ", type=PARTIAL" +
-            ", expr='" + expr + '\'' +
-            ", columns=[" + columns().stream().map(IndexColumn::toString).collect(Collectors.joining(",")) + ']' +
-            ']';
+        return S.toString(PartialIndex.class, this,
+            "type", type(),
+            "name", name(),
+            "uniq", unique(),
+            "cols", columns());
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/PrimaryIndexImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/PrimaryIndexImpl.java
index c99340d..f433531 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/PrimaryIndexImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/PrimaryIndexImpl.java
@@ -19,6 +19,8 @@ package org.apache.ignite.internal.schema;
 
 import java.util.Collections;
 import java.util.List;
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.PrimaryIndex;
 import org.apache.ignite.schema.SortedIndexColumn;
 
@@ -27,6 +29,7 @@ import org.apache.ignite.schema.SortedIndexColumn;
  */
 public class PrimaryIndexImpl extends SortedIndexImpl implements PrimaryIndex {
     /** Affinity columns. */
+    @IgniteToStringInclude
     private final List<String> affCols;
 
     /**
@@ -49,4 +52,18 @@ public class PrimaryIndexImpl extends SortedIndexImpl implements PrimaryIndex {
     @Override public String type() {
         return "PK";
     }
+
+    /** {@inheritDoc} */
+    @Override public boolean unique() {
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(PrimaryIndexImpl.class, this,
+            "type", type(),
+            "name", name(),
+            "uniq", unique(),
+            "cols", columns());
+    }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java
index 15c0962..e0e0dfb 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaDescriptor.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.schema;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.ignite.internal.tostring.S;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -106,4 +107,9 @@ public class SchemaDescriptor {
     public @Nullable Column column(@NotNull String name) {
         return colMap.get(name);
     }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return S.toString(SchemaDescriptor.class, this);
+    }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaTableImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaTableImpl.java
index c4180f9..97589ff 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaTableImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SchemaTableImpl.java
@@ -25,6 +25,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.ignite.internal.schema.modification.TableModificationBuilderImpl;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.Column;
 import org.apache.ignite.schema.IndexColumn;
 import org.apache.ignite.schema.PrimaryIndex;
@@ -134,12 +135,6 @@ public class SchemaTableImpl extends AbstractSchemaObject implements SchemaTable
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "SchemaTable[" +
-            "name='" + name() + '\'' +
-            ", keyCols=" + keyCols.stream().map(Column::name).collect(Collectors.joining(",")) +
-            ", affCols=" + affCols.stream().map(Column::name).collect(Collectors.joining(",")) +
-            ", column=" + cols.values() +
-            ", indices=" + indices.values() +
-            ']';
+        return S.toString(SchemaTableImpl.class, this);
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexColumnImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexColumnImpl.java
index 8df6ae7..a0f7a37 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexColumnImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexColumnImpl.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.internal.schema;
 
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.SortedIndexColumn;
 
 /**
@@ -45,9 +46,6 @@ public class SortedIndexColumnImpl extends AbstractSchemaObject implements Sorte
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "Column[" +
-            "name='" + name() + '\'' +
-            ", order=" + (asc ? "asc" : "desc") +
-            ']';
+        return S.toString(SortedIndexColumnImpl.class, this);
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexImpl.java
index 115f46c..3b5a44a 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/SortedIndexImpl.java
@@ -19,8 +19,8 @@ package org.apache.ignite.internal.schema;
 
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
-import org.apache.ignite.schema.IndexColumn;
+import org.apache.ignite.internal.tostring.IgniteToStringInclude;
+import org.apache.ignite.internal.tostring.S;
 import org.apache.ignite.schema.SortedIndex;
 import org.apache.ignite.schema.SortedIndexColumn;
 
@@ -29,6 +29,7 @@ import org.apache.ignite.schema.SortedIndexColumn;
  */
 public class SortedIndexImpl extends AbstractSchemaObject implements SortedIndex {
     /** Columns. */
+    @IgniteToStringInclude
     private final List<SortedIndexColumn> cols;
 
     /** Unique flag. */
@@ -65,12 +66,8 @@ public class SortedIndexImpl extends AbstractSchemaObject implements SortedIndex
 
     /** {@inheritDoc} */
     @Override public String toString() {
-        return "SortedIndex[" +
-            "name='" + name() + '\'' +
-            ", type=SORTED" +
-            ", uniq=" + uniq +
-            ", columns=[" + columns().stream().map(IndexColumn::toString).collect(Collectors.joining(",")) +
-            "]]";
+        return S.toString(SortedIndex.class, this,
+            "type", type(),
+            "name", name());
     }
-
 }