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

shuber pushed a commit to branch unomi-3-dev
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/unomi-3-dev by this push:
     new 9f938ff92 UNOMI-881 Add LogChecker for InputValidationIT to filter 
expected validation errors in logs. Optimize LogChecker for performance with 
hierarchical matching and multi-part substrings.
9f938ff92 is described below

commit 9f938ff9232a3feb3d4e82f23190a8287c09c0d1
Author: Serge Huber <[email protected]>
AuthorDate: Thu Jan 1 16:18:47 2026 +0100

    UNOMI-881 Add LogChecker for InputValidationIT to filter expected 
validation errors in logs. Optimize LogChecker for performance with 
hierarchical matching and multi-part substrings.
---
 .../test/java/org/apache/unomi/itests/BaseIT.java  | 254 ++----
 .../unomi/itests/CopyPropertiesActionIT.java       |  19 +-
 .../org/apache/unomi/itests/InputValidationIT.java |  29 +-
 .../java/org/apache/unomi/itests/JSONSchemaIT.java |  31 +
 .../org/apache/unomi/itests/tools/LogChecker.java  | 860 ++++++++++++++-------
 .../apache/unomi/itests/tools/LogCheckerTest.java  | 396 ++++++++++
 6 files changed, 1114 insertions(+), 475 deletions(-)

diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java 
b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index 234ccace6..424f331b5 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -22,7 +22,11 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Route;
+import org.apache.camel.ServiceStatus;
 import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpEntity;
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.*;
 import org.apache.http.config.Registry;
@@ -30,14 +34,13 @@ import org.apache.http.config.RegistryBuilder;
 import org.apache.http.conn.socket.ConnectionSocketFactory;
 import org.apache.http.conn.socket.PlainConnectionSocketFactory;
 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.BufferedHttpEntity;
 import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
-import org.apache.http.HttpEntity;
 import org.apache.http.impl.client.BasicCredentialsProvider;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.apache.http.impl.client.HttpClientBuilder;
 import org.apache.http.impl.client.HttpClients;
-import org.apache.http.entity.BufferedHttpEntity;
 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 import org.apache.karaf.itests.KarafTestSupport;
 import org.apache.unomi.api.Item;
@@ -52,22 +55,19 @@ import org.apache.unomi.api.tenants.Tenant;
 import org.apache.unomi.api.tenants.TenantService;
 import org.apache.unomi.api.utils.ConditionBuilder;
 import org.apache.unomi.groovy.actions.services.GroovyActionsService;
-import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
 import org.apache.unomi.itests.tools.LogChecker;
+import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
 import org.apache.unomi.lifecycle.BundleWatcher;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
 import org.apache.unomi.router.api.ExportConfiguration;
 import org.apache.unomi.router.api.IRouterCamelContext;
-import org.apache.camel.CamelContext;
-import org.apache.camel.Route;
-import org.apache.camel.ServiceStatus;
 import org.apache.unomi.router.api.ImportConfiguration;
 import org.apache.unomi.router.api.services.ImportExportConfigurationService;
 import org.apache.unomi.schema.api.SchemaService;
 import org.apache.unomi.services.UserListService;
 import org.apache.unomi.shell.services.UnomiManagementService;
-import org.apache.unomi.rest.authentication.RestAuthenticationConfig;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -102,7 +102,6 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.KeyManagementException;
-import java.util.Hashtable;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
 import java.security.cert.X509Certificate;
@@ -209,19 +208,13 @@ public abstract class BaseIT extends KarafTestSupport {
      */
     protected void checkSearchEngine() {
         searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, 
SEARCH_ENGINE_ELASTICSEARCH);
-        // Fix elasticsearch-maven-plugin default_template issue before any 
test setup
-        // The plugin creates a default_template with very high priority that 
overrides all user templates
-        // This must be done very early, before Unomi starts or any migration 
runs
-        if (SEARCH_ENGINE_ELASTICSEARCH.equals(searchEngine)) {
-            fixDefaultTemplateIfNeeded();
-        }
     }
 
     @Before
     public void waitForStartup() throws InterruptedException {
         // disable retry
         retry = new KarafTestSupport.Retry(false);
-        
+
         // Check search engine and apply any necessary fixes (e.g., 
default_template deletion)
         checkSearchEngine();
 
@@ -301,14 +294,16 @@ public abstract class BaseIT extends KarafTestSupport {
 
         // init httpClient without credentials provider - all auth handled via 
headers
         httpClient = initHttpClient(null);
-        
+
         // Initialize log checker if enabled
         if (isLogCheckingEnabled()) {
-            logChecker = new LogChecker();
+            // Use builder API - by default enable all patterns for backward 
compatibility
+            // Individual tests can override createLogChecker() to specify 
only needed patterns
+            logChecker = createLogChecker();
             LOGGER.info("Log checking enabled using in-memory appender");
         }
     }
-    
+
     /**
      * Mark log checkpoint before each test
      * This method is called automatically by JUnit before each test method
@@ -356,7 +351,7 @@ public abstract class BaseIT extends KarafTestSupport {
     public void shutdown() {
         // Check logs for unexpected errors/warnings before cleanup
         checkLogsForUnexpectedIssues();
-        
+
         if (testTenant != null) {
             try {
                 tenantService.deleteTenant(testTenant.getItemId());
@@ -371,7 +366,35 @@ public abstract class BaseIT extends KarafTestSupport {
         httpClient = null;
     }
 
-    
+
+    /**
+     * Create a LogChecker instance. Tests should override this method to add
+     * only the patterns they need, improving performance significantly.
+     *
+     * By default, only global patterns are included (e.g., BundleWatcher 
warnings).
+     *
+     * IMPORTANT: Prefer literal strings over regex for better performance.
+     * Literal strings use fast contains() matching instead of regex.
+     *
+     * Example override for a test that needs specific substrings:
+     * <pre>
+     * {@literal @}Override
+     * protected LogChecker createLogChecker() {
+     *     return LogChecker.builder()
+     *         .addIgnoredSubstring("Response status code: 400")               
 // Single substring
+     *         .addIgnoredMultiPart("Schema", "not found")                     
// Multi-part: sequential
+     *         .build();
+     * }
+     * </pre>
+     *
+     * @return A configured LogChecker instance
+     */
+    protected LogChecker createLogChecker() {
+        // By default, only global patterns are included
+        // Individual tests should override this to add their specific patterns
+        return new LogChecker();
+    }
+
     /**
      * Check logs for unexpected errors and warnings since the last checkpoint
      * This is called automatically after each test
@@ -380,20 +403,20 @@ public abstract class BaseIT extends KarafTestSupport {
         if (logChecker == null) {
             return;
         }
-        
+
         try {
             LogChecker.LogCheckResult result = 
logChecker.checkLogsSinceLastCheckpoint();
-            
+
             if (result.hasUnexpectedIssues()) {
                 String summary = result.getSummary();
                 String testInfo = currentTestName != null ? "Test: " + 
currentTestName + "\n" : "";
-                
+
                 // Use System.err/out to avoid creating logs that would be 
captured by InMemoryLogAppender
                 // This prevents a feedback loop where log checking creates 
more logs to check
                 System.err.println("\n=== UNEXPECTED LOG ISSUES DETECTED ===");
                 System.err.println(testInfo + summary);
                 
System.err.println("=======================================\n");
-                
+
                 // Add to JUnit test output by printing to System.out 
(captured by JUnit)
                 System.out.println("\n=== SERVER-SIDE LOG ISSUES ===");
                 System.out.println(testInfo + summary);
@@ -405,7 +428,7 @@ public abstract class BaseIT extends KarafTestSupport {
             e.printStackTrace(System.err);
         }
     }
-    
+
     /**
      * Check if log checking is enabled
      * Can be controlled via system property: it.unomi.log.checking.enabled
@@ -415,25 +438,25 @@ public abstract class BaseIT extends KarafTestSupport {
         String enabled = System.getProperty(ENABLE_LOG_CHECKING_PROPERTY, 
"true");
         return Boolean.parseBoolean(enabled);
     }
-    
+
     /**
-     * Add a pattern to ignore for log checking
+     * Add a substring to ignore for log checking
      * Useful for tests that expect certain errors/warnings
-     * @param pattern Regex pattern to match against log messages
+     * @param substring Literal substring or regex pattern to match against 
log messages
      */
-    protected void addIgnoredLogPattern(String pattern) {
+    protected void addIgnoredLogSubstring(String substring) {
         if (logChecker != null) {
-            logChecker.addIgnoredPattern(pattern);
+            logChecker.addIgnoredSubstring(substring);
         }
     }
-    
+
     /**
-     * Add multiple patterns to ignore for log checking
-     * @param patterns List of regex patterns
+     * Add multiple substrings to ignore for log checking
+     * @param substrings List of substrings (literal or regex)
      */
-    protected void addIgnoredLogPatterns(List<String> patterns) {
+    protected void addIgnoredLogSubstrings(List<String> substrings) {
         if (logChecker != null) {
-            logChecker.addIgnoredPatterns(patterns);
+            logChecker.addIgnoredSubstrings(substrings);
         }
     }
 
@@ -698,11 +721,11 @@ public abstract class BaseIT extends KarafTestSupport {
             LOGGER.info("Configuring in-memory log appender for log checking");
             // Configure the appender in Log4j2
             // The appender is already available via the log4j-extension 
fragment bundle
-            
karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", 
+            
karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg",
                 "log4j2.appender.inMemory.type", "InMemoryLogAppender"));
-            
karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", 
+            
karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg",
                 "log4j2.appender.inMemory.name", "InMemoryLogAppender"));
-            
karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", 
+            
karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg",
                 "log4j2.rootLogger.appenderRef.inMemory.ref", 
"InMemoryLogAppender"));
         }
 
@@ -1421,138 +1444,17 @@ public abstract class BaseIT extends KarafTestSupport {
         return response;
     }
 
-    /**
-     * Fixes the default_template created by elasticsearch-maven-plugin that 
overrides all user templates.
-     * The plugin creates a template with index_patterns: ["*"] and very high 
priority (2147483520),
-     * which in ES 8/9 overrides all other templates since composable 
templates don't merge.
-     * This method detects and deletes the template if it has a very high 
priority.
-     * This must be called before Unomi starts since the ES persistence 
service isn't available yet.
-     */
-    private void fixDefaultTemplateIfNeeded() {
-        String templateName = "default_template";
-        String esBaseUrl = "http://localhost:"; + getSearchPort();
-        String templateUrl = esBaseUrl + "/_index_template/" + templateName;
-        
-        CloseableHttpClient tempHttpClient = null;
-        try {
-            // Create a temporary HTTP client for ES requests
-            tempHttpClient = initHttpClient(null);
-            
-            // Check if default_template exists using HEAD request
-            HttpHead headRequest = new HttpHead(templateUrl);
-            CloseableHttpResponse headResponse = null;
-            try {
-                headResponse = tempHttpClient.execute(headRequest);
-                int statusCode = headResponse.getStatusLine().getStatusCode();
-                
-                if (statusCode == 404) {
-                    // Template doesn't exist, nothing to fix
-                    LOGGER.debug("default_template does not exist, no action 
needed");
-                    return;
-                } else if (statusCode != 200) {
-                    // Unexpected status, log and continue
-                    LOGGER.warn("Unexpected status code {} when checking for 
default_template, skipping fix", statusCode);
-                    return;
-                }
-            } finally {
-                if (headResponse != null) {
-                    headResponse.close();
-                }
-            }
-            
-            // Template exists, get its details to check priority
-            HttpGet getRequest = new HttpGet(templateUrl);
-            CloseableHttpResponse getResponse = null;
-            try {
-                getResponse = tempHttpClient.execute(getRequest);
-                int statusCode = getResponse.getStatusLine().getStatusCode();
-                
-                if (statusCode == 200) {
-                    String responseBody = 
IOUtils.toString(getResponse.getEntity().getContent(), "UTF-8");
-                    JsonNode jsonNode = objectMapper.readTree(responseBody);
-                    
-                    // Parse the template response
-                    // ES API returns: {"index_templates": [{"name": 
"default_template", "index_template": {...}}]}
-                    if (jsonNode.has("index_templates") && 
jsonNode.get("index_templates").isArray()) {
-                        JsonNode templates = jsonNode.get("index_templates");
-                        for (JsonNode template : templates) {
-                            if (template.has("name") && 
templateName.equals(template.get("name").asText())) {
-                                JsonNode indexTemplate = 
template.get("index_template");
-                                if (indexTemplate != null && 
indexTemplate.has("priority")) {
-                                    Long priority = 
indexTemplate.get("priority").asLong();
-                                    
-                                    // Check if priority is very high (>= 
2147480000, near Integer.MAX_VALUE)
-                                    // This indicates it's the problematic 
template from elasticsearch-maven-plugin
-                                    if (priority >= 2147480000L) {
-                                        LOGGER.warn("Detected default_template 
with very high priority ({}). " +
-                                                "This template from 
elasticsearch-maven-plugin overrides all user templates in ES 8/9. " +
-                                                "Deleting it to allow user 
templates to work correctly.", priority);
-                                        
-                                        // Delete the template
-                                        HttpDelete deleteRequest = new 
HttpDelete(templateUrl);
-                                        CloseableHttpResponse deleteResponse = 
null;
-                                        try {
-                                            deleteResponse = 
tempHttpClient.execute(deleteRequest);
-                                            int deleteStatusCode = 
deleteResponse.getStatusLine().getStatusCode();
-                                            
-                                            if (deleteStatusCode == 200) {
-                                                // Parse delete response to 
check acknowledged
-                                                String deleteResponseBody = 
IOUtils.toString(deleteResponse.getEntity().getContent(), "UTF-8");
-                                                JsonNode deleteJsonNode = 
objectMapper.readTree(deleteResponseBody);
-                                                boolean acknowledged = 
deleteJsonNode.has("acknowledged") && 
-                                                                       
deleteJsonNode.get("acknowledged").asBoolean();
-                                                
-                                                if (acknowledged) {
-                                                    LOGGER.info("Successfully 
deleted default_template. User templates will now work correctly.");
-                                                } else {
-                                                    LOGGER.warn("Failed to 
delete default_template - not acknowledged. User templates may not work 
correctly.");
-                                                }
-                                            } else {
-                                                LOGGER.warn("Failed to delete 
default_template - status code: {}. User templates may not work correctly.", 
deleteStatusCode);
-                                            }
-                                        } finally {
-                                            if (deleteResponse != null) {
-                                                deleteResponse.close();
-                                            }
-                                        }
-                                    } else {
-                                        LOGGER.debug("default_template exists 
but has normal priority ({}), no action needed.", priority);
-                                    }
-                                    break; // Found the template, no need to 
continue
-                                }
-                            }
-                        }
-                    }
-                } else {
-                    LOGGER.warn("Failed to get default_template details - 
status code: {}, skipping fix", statusCode);
-                }
-            } finally {
-                if (getResponse != null) {
-                    getResponse.close();
-                }
-            }
-        } catch (Exception e) {
-            // Log but don't fail startup - this is a best-effort fix for 
integration tests
-            LOGGER.warn("Failed to check/fix default_template: {}. This may 
affect template application in integration tests.", 
-                    e.getMessage(), e);
-        } finally {
-            if (tempHttpClient != null) {
-                closeHttpClient(tempHttpClient);
-            }
-        }
-    }
-
     /**
      * Enables Camel tracing and debug logging if requested via system 
property.
      * This provides visibility into Camel operations during test execution 
without modifying production code.
-     * 
+     *
      * To enable: Set system property -Dit.unomi.camel.debug=true
-     * 
+     *
      * This will:
      * - Enable Camel tracing (logs detailed message flow, body content, 
headers as messages traverse routes)
      *   Tracing is useful for understanding WHAT is happening in routes 
(message content, transformations)
      * - Enable DEBUG logging for Camel packages (configured in config() 
method)
-     * 
+     *
      * Note: Tracing provides different information than route status checking:
      * - Tracing: Shows message flow and content (useful for debugging message 
transformations)
      * - Route Status API: Shows if routes are running, exchange counts, 
processing times (useful for verifying execution)
@@ -1576,7 +1478,7 @@ public abstract class BaseIT extends KarafTestSupport {
      * Gets the Camel context from the router Camel context service.
      * Uses the interface method which returns Object to avoid exposing Camel 
dependency.
      * Based on official Camel API: https://camel.apache.org/manual/
-     * 
+     *
      * @return The CamelContext instance, or null if not available
      */
     protected CamelContext getCamelContext() {
@@ -1593,7 +1495,7 @@ public abstract class BaseIT extends KarafTestSupport {
     /**
      * Checks if a Camel route with the given route ID exists.
      * Uses official Camel API: CamelContext.getRoute(String routeId)
-     * 
+     *
      * @param routeId The route ID to check (typically the import 
configuration itemId)
      * @return true if the route exists, false otherwise
      */
@@ -1610,7 +1512,7 @@ public abstract class BaseIT extends KarafTestSupport {
      * Gets the status of a Camel route.
      * Uses Camel 2.23.1 API directly.
      * Returns ServiceStatus enum: Started, Stopped, Suspended, etc.
-     * 
+     *
      * @param routeId The route ID to get status for
      * @return The route status, or null if route doesn't exist or status 
unavailable
      */
@@ -1637,7 +1539,7 @@ public abstract class BaseIT extends KarafTestSupport {
     /**
      * Checks if a Camel route is started (running).
      * Uses official Camel API to check route status.
-     * 
+     *
      * @param routeId The route ID to check
      * @return true if the route exists and is started, false otherwise
      */
@@ -1649,7 +1551,7 @@ public abstract class BaseIT extends KarafTestSupport {
     /**
      * Gets detailed information about a Camel route including status, 
endpoints, and configuration.
      * Uses Camel 2.23.1 API to inspect route definitions and endpoints.
-     * 
+     *
      * @param routeId The route ID to get information for
      * @return A string describing the route status, endpoints, and 
configuration, or error message if route doesn't exist
      */
@@ -1663,10 +1565,10 @@ public abstract class BaseIT extends KarafTestSupport {
             if (route == null) {
                 return "Route '" + routeId + "' does not exist";
             }
-            
+
             StringBuilder info = new StringBuilder();
             info.append("Route '").append(routeId).append("': ");
-            
+
             // Get route status using official API
             ServiceStatus status = getCamelRouteStatus(routeId);
             if (status != null) {
@@ -1674,7 +1576,7 @@ public abstract class BaseIT extends KarafTestSupport {
             } else {
                 info.append("status=unknown");
             }
-            
+
             // Get route definition to inspect endpoints and configuration
             try {
                 org.apache.camel.model.RouteDefinition routeDefinition = 
camelContext.getRouteDefinition(routeId);
@@ -1687,7 +1589,7 @@ public abstract class BaseIT extends KarafTestSupport {
                             info.append(", from=").append(from.getUri());
                         }
                     }
-                    
+
                     // Get output endpoints (to)
                     
java.util.List<org.apache.camel.model.ProcessorDefinition<?>> outputs = 
routeDefinition.getOutputs();
                     if (outputs != null && !outputs.isEmpty()) {
@@ -1714,10 +1616,10 @@ public abstract class BaseIT extends KarafTestSupport {
                 // Route definition inspection failed, that's okay
                 LOGGER.debug("Could not get route definition for {}: {}", 
routeId, e.getMessage());
             }
-            
+
             // Note: Management statistics (exchange counts, processing times) 
require camel-management dependency.
             // For test visibility, route status and endpoint information are 
the most useful.
-            
+
             return info.toString();
         } catch (Exception e) {
             return "Error getting route info for '" + routeId + "': " + 
e.getMessage();
@@ -1727,7 +1629,7 @@ public abstract class BaseIT extends KarafTestSupport {
     /**
      * Waits for a Camel route to be created and started.
      * This is useful for tests that need to verify the route was created by 
the timer.
-     * 
+     *
      * @param routeId The route ID to wait for
      * @param timeoutMs Timeout in milliseconds between retries
      * @param maxRetries Maximum number of retries
@@ -1751,7 +1653,7 @@ public abstract class BaseIT extends KarafTestSupport {
     /**
      * Gets a list of all Camel route IDs with their statuses.
      * Uses official Camel API: CamelContext.getRoutes()
-     * 
+     *
      * @return Map of route ID to status, or empty map if CamelContext is not 
available
      */
     protected java.util.Map<String, ServiceStatus> 
getAllCamelRoutesWithStatus() {
@@ -1779,7 +1681,7 @@ public abstract class BaseIT extends KarafTestSupport {
 
     /**
      * Gets a list of all Camel route IDs.
-     * 
+     *
      * @return List of route IDs, or empty list if CamelContext is not 
available
      */
     protected java.util.List<String> getAllCamelRouteIds() {
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java 
b/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java
index 6acde9fe2..6ff09cee9 100644
--- a/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java
@@ -22,6 +22,7 @@ import org.apache.unomi.api.Metadata;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.PropertyType;
 import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.itests.tools.LogChecker;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.junit.After;
 import org.junit.Assert;
@@ -36,13 +37,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
+import java.util.*;
 
 /**
  * Created by amidani on 12/10/2017.
@@ -60,6 +55,16 @@ public class CopyPropertiesActionIT extends BaseIT {
     public static final String PROPERTY_TO_MAP = "PropertyToMap";
     public static final String MAPPED_PROPERTY = "MappedProperty";
 
+    /**
+     * Configure LogChecker with substrings for expected property copy errors 
in this test.
+     */
+    @Override
+    protected LogChecker createLogChecker() {
+        return LogChecker.builder()
+            .addIgnoredSubstring("Impossible to copy the property")
+            .build();
+    }
+
     @Before
     public void setUp() throws InterruptedException {
         Profile profile = new Profile();
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java 
b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
index 4f78231f5..316ca5354 100644
--- a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
@@ -24,8 +24,11 @@ import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.util.EntityUtils;
 import org.apache.unomi.api.Scope;
+import org.apache.unomi.itests.tools.LogChecker;
 import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
-import org.junit.*;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
@@ -52,6 +55,30 @@ public class InputValidationIT extends BaseIT {
     private final static String ERROR_MESSAGE_INVALID_DATA_RECEIVED = "Request 
rejected by the server because: Invalid received data";
     public static final String DUMMY_SCOPE = "dummy_scope";
 
+    /**
+     * Configure LogChecker with substrings for expected validation errors in 
this test.
+     * These are errors that are intentionally triggered to test validation 
logic.
+     */
+    @Override
+    protected LogChecker createLogChecker() {
+        return LogChecker.builder()
+            // InvalidRequestExceptionMapper errors (expected when testing 
invalid requests)
+            .addIgnoredSubstring("InvalidRequestExceptionMapper")
+            .addIgnoredSubstring("Invalid parameter")
+            .addIgnoredSubstring("Invalid Context request object")
+            .addIgnoredSubstring("Invalid events collector object")
+            .addIgnoredSubstring("Invalid profile ID format in cookie")
+            .addIgnoredSubstring("events collector cannot be empty")
+            .addIgnoredSubstring("Unable to deserialize object because")
+            // RequestValidatorInterceptor warnings (expected when testing 
request size limits)
+            .addIgnoredSubstring("RequestValidatorInterceptor")
+            .addIgnoredSubstring("has thrown exception, unwinding now")
+            .addIgnoredSubstring("exceeding maximum bytes size")
+            .addIgnoredSubstring("Incoming POST request blocked because 
exceeding maximum bytes size")
+            .addIgnoredSubstring("Response status code: 400")
+            .build();
+    }
+
     @Before
     public void setUp() throws InterruptedException {
         TestUtils.createScope(DUMMY_SCOPE, "Dummy scope", scopeService);
diff --git a/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java 
b/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java
index d0504f29c..e37c23cc8 100644
--- a/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java
@@ -25,6 +25,7 @@ import org.apache.http.util.EntityUtils;
 import org.apache.unomi.api.Event;
 import org.apache.unomi.api.Scope;
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.itests.tools.LogChecker;
 import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
 import org.apache.unomi.schema.api.JsonSchemaWrapper;
 import org.apache.unomi.schema.api.ValidationError;
@@ -58,6 +59,36 @@ public class JSONSchemaIT extends BaseIT {
     private static final int DEFAULT_TRYING_TRIES = 30;
     public static final String DUMMY_SCOPE = "dummy_scope";
 
+    /**
+     * Configure LogChecker with substrings for expected schema-related errors 
in this test.
+     * These are errors that are intentionally triggered to test schema 
validation logic.
+     */
+    @Override
+    protected LogChecker createLogChecker() {
+        return LogChecker.builder()
+            // Schema not found errors (expected when testing with missing 
schemas)
+            .addIgnoredSubstring("Schema not found for event type: dummy")
+            .addIgnoredSubstring("Schema not found for event type: flattened")
+            .addIgnoredSubstring("Couldn't find schema")
+            .addIgnoredSubstring("Failed to load json schema")
+            // Schema validation errors (expected when testing invalid events)
+            .addIgnoredSubstring("Schema validation found")
+            .addIgnoredSubstring("Validation error")
+            .addIgnoredSubstring("does not match the regex pattern")
+            .addIgnoredSubstring("There are unevaluated properties")
+            .addIgnoredSubstring("Unknown scope value")
+            .addIgnoredSubstring("may only have a maximum of")
+            .addIgnoredSubstring("string found, number expected")
+            // Schema-related exceptions (expected during schema operations)
+            .addIgnoredSubstring("JsonSchemaException")
+            .addIgnoredSubstring("InvocationTargetException")
+            .addIgnoredSubstring("IOException")
+            .addIgnoredSubstring("Error executing system operation: Test 
exception")
+            .addIgnoredSubstring("Couldn't find persona")
+            .addIgnoredSubstring("Unable to save schema")
+            .build();
+    }
+
     @Before
     public void setUp() throws InterruptedException {
         keepTrying("Couldn't find json schema endpoint", () -> 
get(JSONSCHEMA_URL, List.class), Objects::nonNull, DEFAULT_TRYING_TIMEOUT,
diff --git a/itests/src/test/java/org/apache/unomi/itests/tools/LogChecker.java 
b/itests/src/test/java/org/apache/unomi/itests/tools/LogChecker.java
index 50bdacad8..dada4a5bb 100644
--- a/itests/src/test/java/org/apache/unomi/itests/tools/LogChecker.java
+++ b/itests/src/test/java/org/apache/unomi/itests/tools/LogChecker.java
@@ -27,18 +27,41 @@ import java.util.regex.Pattern;
 /**
  * Utility class to check logs for unexpected errors and warnings using an 
in-memory appender.
  * This replaces the file-based log checker and works with PaxExam/Karaf 
integration tests.
+ *
+ * PERFORMANCE: To avoid checking 43,000+ log entries against many patterns, 
each test class
+ * should add only the patterns it needs. Prefer literal strings over regex 
for better performance.
+ *
+ * Example usage in a test class:
+ * <pre>
+ * {@literal @}Override
+ * protected LogChecker createLogChecker() {
+ *     return LogChecker.builder()
+ *         .addIgnoredSubstring("Response status code: 400")                // 
Single substring (fast)
+ *         .addIgnoredMultiPart("Schema", "not found")                     // 
Multi-part: "Schema" then "not found"
+ *         .addIgnoredMultiPart("Invalid", "parameter", "format")          // 
Multi-part: all must appear in order
+ *         .build();
+ * }
+ * </pre>
+ *
+ * IMPORTANT: All substrings are literal (no regex). Uses fast hierarchical 
prefix-based matching
+ * with tree structure for multi-part patterns. Only checks subsequent parts 
if first part matches,
+ * avoiding backtracking and multiple passes. Optimized for processing 43,000+ 
log entries.
  */
 public class LogChecker {
-    
+
     private int checkpointIndex = 0;
-    private final Set<Pattern> ignoredPatterns;
-    private final Map<Pattern, java.util.regex.Matcher> matcherCache;
-    private final List<String> literalPatterns; // Fast path for literal 
string patterns (case-insensitive)
+    private final LiteralPatternMatcher literalSubstringMatcher; // 
Hierarchical prefix-based matcher for literal substrings
     private final int errorContextLinesBefore;
     private final int errorContextLinesAfter;
     private final int warningContextLinesBefore;
     private final int warningContextLinesAfter;
-    
+
+    // Maximum length of candidate string for pattern matching to prevent 
processing extremely long strings
+    private static final int MAX_CANDIDATE_LENGTH = 10000; // 10KB limit
+
+    // Prefix length for hierarchical matching - balances between selectivity 
and overhead
+    private static final int PREFIX_LENGTH = 4;
+
     /**
      * Simple data class to hold context event information (avoids storing 
Log4j2 core classes)
      */
@@ -48,7 +71,7 @@ public class LogChecker {
         final String thread;
         final String logger;
         final String message;
-        
+
         ContextEvent(String timestamp, String level, String thread, String 
logger, String message) {
             this.timestamp = timestamp;
             this.level = level;
@@ -56,13 +79,13 @@ public class LogChecker {
             this.logger = logger;
             this.message = message;
         }
-        
+
         String format(LogChecker checker) {
-            return String.format("%s [%s] %s - %s", 
+            return String.format("%s [%s] %s - %s",
                 checker.formatTimestamp(timestamp), level, 
checker.shortenLogger(logger), checker.truncateMessage(message, 100));
         }
     }
-    
+
     /**
      * Represents a log entry with its details including context
      */
@@ -76,7 +99,7 @@ public class LogChecker {
         private final List<String> stacktrace;
         private final List<ContextEvent> contextBefore;
         private final List<ContextEvent> contextAfter;
-        
+
         public LogEntry(String timestamp, String level, String thread, String 
logger, String message, long lineNumber) {
             this.timestamp = timestamp;
             this.level = level;
@@ -88,7 +111,7 @@ public class LogChecker {
             this.contextBefore = new ArrayList<>();
             this.contextAfter = new ArrayList<>();
         }
-        
+
         public String getTimestamp() { return timestamp; }
         public String getLevel() { return level; }
         public String getThread() { return thread; }
@@ -98,26 +121,26 @@ public class LogChecker {
         public List<String> getStacktrace() { return stacktrace; }
         public List<ContextEvent> getContextBefore() { return contextBefore; }
         public List<ContextEvent> getContextAfter() { return contextAfter; }
-        
+
         public void addStacktraceLine(String line) {
             stacktrace.add(line);
         }
-        
+
         public void addContextBefore(ContextEvent event) {
             contextBefore.add(event);
         }
-        
+
         public void addContextAfter(ContextEvent event) {
             contextAfter.add(event);
         }
-        
+
         public String getFullMessage() {
             if (stacktrace.isEmpty()) {
                 return message;
             }
             return message + "\n" + String.join("\n", stacktrace);
         }
-        
+
         public String getFullContext() {
             StringBuilder sb = new StringBuilder();
             appendContextBefore(sb);
@@ -126,7 +149,7 @@ public class LogChecker {
             appendContextAfter(sb);
             return sb.toString();
         }
-        
+
         private void appendContextBefore(StringBuilder sb) {
             if (!contextBefore.isEmpty()) {
                 sb.append("--- Context before (")
@@ -136,20 +159,20 @@ public class LogChecker {
                 }
             }
         }
-        
+
         private void appendIssueLine(StringBuilder sb) {
             String headerLevel = (level != null) ? level : "LOG";
             LogChecker checker = LogChecker.this;
-            
+
             // Extract source location from stack trace
             String sourceLocation = checker.extractSourceLocation(stacktrace);
-            
+
             // Compact format: time [level] thread L{logLine} -> 
sourceLocation: message
             String time = checker.formatTimestamp(timestamp);
             String shortThread = checker.shortenThread(thread);
             String shortLogger = checker.shortenLogger(logger);
             String truncatedMsg = checker.truncateMessage(message, 200);
-            
+
             // Format: time [level] thread L{logLine} -> ClassName:line: 
message
             if (sourceLocation != null && !sourceLocation.isEmpty()) {
                 sb.append(String.format("%s [%s] %s L%d -> %s: %s",
@@ -159,7 +182,7 @@ public class LogChecker {
                     time, headerLevel, shortThread, lineNumber, shortLogger, 
truncatedMsg));
             }
         }
-        
+
         private void appendStackTrace(StringBuilder sb) {
             if (!stacktrace.isEmpty()) {
                 sb.append("\n");
@@ -168,7 +191,7 @@ public class LogChecker {
                 }
             }
         }
-        
+
         private void appendContextAfter(StringBuilder sb) {
             if (!contextAfter.isEmpty()) {
                 sb.append("\n--- Context after (")
@@ -178,14 +201,14 @@ public class LogChecker {
                 }
             }
         }
-        
+
         @Override
         public String toString() {
-            return String.format("[%s] %s [%s] %s - %s (line %d)", 
+            return String.format("[%s] %s [%s] %s - %s (line %d)",
                 timestamp, level, thread, logger, message, lineNumber);
         }
     }
-    
+
     /**
      * Result of a log check
      */
@@ -193,17 +216,17 @@ public class LogChecker {
         private final List<LogEntry> errors;
         private final List<LogEntry> warnings;
         private final boolean hasUnexpectedIssues;
-        
+
         public LogCheckResult(List<LogEntry> errors, List<LogEntry> warnings) {
             this.errors = errors != null ? errors : Collections.emptyList();
             this.warnings = warnings != null ? warnings : 
Collections.emptyList();
             this.hasUnexpectedIssues = !this.errors.isEmpty() || 
!this.warnings.isEmpty();
         }
-        
+
         public List<LogEntry> getErrors() { return errors; }
         public List<LogEntry> getWarnings() { return warnings; }
         public boolean hasUnexpectedIssues() { return hasUnexpectedIssues; }
-        
+
         public String getSummary() {
             if (!hasUnexpectedIssues) {
                 return "No unexpected errors or warnings found in logs.";
@@ -213,7 +236,7 @@ public class LogChecker {
             appendWarningsSummary(sb);
             return sb.toString();
         }
-        
+
         private void appendErrorsSummary(StringBuilder sb) {
             if (!errors.isEmpty()) {
                 sb.append(String.format("Found %d error(s):", errors.size()));
@@ -227,7 +250,7 @@ public class LogChecker {
                 }
             }
         }
-        
+
         private void appendWarningsSummary(StringBuilder sb) {
             if (!warnings.isEmpty()) {
                 sb.append(String.format("\nFound %d warning(s):", 
warnings.size()));
@@ -242,7 +265,7 @@ public class LogChecker {
             }
         }
     }
-    
+
     /**
      * Create a new LogChecker with default context lines:
      * - Errors: 10 lines before and after
@@ -251,212 +274,463 @@ public class LogChecker {
     public LogChecker() {
         this(10, 10, 0, 0);
     }
-    
+
     /**
-     * Create a new LogChecker with custom context line settings
+     * Create a new LogChecker with custom context line settings.
+     * Only includes truly global patterns that occur in all tests.
+     *
      * @param errorContextLinesBefore Number of lines to capture before each 
error
      * @param errorContextLinesAfter Number of lines to capture after each 
error
      * @param warningContextLinesBefore Number of lines to capture before each 
warning
      * @param warningContextLinesAfter Number of lines to capture after each 
warning
      */
-    public LogChecker(int errorContextLinesBefore, int errorContextLinesAfter, 
+    public LogChecker(int errorContextLinesBefore, int errorContextLinesAfter,
                       int warningContextLinesBefore, int 
warningContextLinesAfter) {
-        this.ignoredPatterns = new HashSet<>();
-        this.matcherCache = new HashMap<>();
-        this.literalPatterns = new ArrayList<>();
+        this.literalSubstringMatcher = new LiteralPatternMatcher();
         this.errorContextLinesBefore = errorContextLinesBefore;
         this.errorContextLinesAfter = errorContextLinesAfter;
         this.warningContextLinesBefore = warningContextLinesBefore;
         this.warningContextLinesAfter = warningContextLinesAfter;
-        addDefaultIgnoredPatterns();
+        // No global substrings needed - BundleWatcher is handled by fast path 
check
     }
-    
+
     /**
-     * Add a pattern to ignore (expected errors/warnings)
-     * @param pattern Regex pattern to match against log messages, or literal 
string (no regex special chars)
+     * Hierarchical prefix-based matcher for literal substrings with support 
for multi-part matching.
+     *
+     * Supports both:
+     * - Single substrings: "Schema not found"
+     * - Multi-part substrings: ["Schema", "not found"] - must appear in 
sequence
+     *
+     * Strategy:
+     * 1. Group by first substring's prefix (first PREFIX_LENGTH chars, or 
full string if shorter)
+     * 2. Build tree: first substring -> list of remaining parts
+     * 3. When matching: only check subsequent parts if first part matches
+     * 4. Single pass through candidate string, no backtracking
+     *
+     * This avoids checking every pattern against every string position,
+     * and avoids checking subsequent parts unless the first part matches.
      */
-    public void addIgnoredPattern(String pattern) {
-        // Check if pattern is a literal string (no regex special characters 
except . which we allow)
-        if (isLiteralPattern(pattern)) {
-            // Use fast string contains() for literal patterns
-            literalPatterns.add(pattern.toLowerCase());
-        } else {
-            // Optimize regex pattern: use possessive quantifiers to prevent 
backtracking
-            String optimizedPattern = optimizePattern(pattern);
-            Pattern compiledPattern = Pattern.compile(optimizedPattern, 
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
-            ignoredPatterns.add(compiledPattern);
-            // Pre-create matcher for this pattern to avoid allocation during 
matching
-            matcherCache.put(compiledPattern, compiledPattern.matcher(""));
+    private static class LiteralPatternMatcher {
+        /**
+         * Represents a multi-part substring match requirement.
+         * First part must match, then subsequent parts must appear in order 
after it.
+         */
+        private static class MultiPartMatch {
+            final String firstPart;           // First substring to match
+            final List<String> remainingParts; // Subsequent substrings (in 
order, after first)
+
+            MultiPartMatch(String firstPart, List<String> remainingParts) {
+                this.firstPart = firstPart;
+                this.remainingParts = remainingParts != null ? remainingParts 
: Collections.emptyList();
+            }
+        }
+
+        // Map from prefix to list of multi-part matches
+        // For patterns with first part >= PREFIX_LENGTH: prefix is first 
PREFIX_LENGTH chars
+        // For patterns with first part < PREFIX_LENGTH: prefix is the entire 
first part
+        private final Map<String, List<MultiPartMatch>> matchesByPrefix = new 
HashMap<>();
+        // Set of first characters of all prefixes (for quick filtering to 
skip most positions)
+        private final Set<Character> prefixFirstChars = new HashSet<>();
+
+        /**
+         * Add a single substring to match
+         */
+        void addPattern(String substring) {
+            addMultiPartPattern(Collections.singletonList(substring));
+        }
+
+        /**
+         * Add a multi-part substring pattern (substrings must appear in 
sequence).
+         *
+         * @param parts List of substrings that must appear in order
+         */
+        void addMultiPartPattern(List<String> parts) {
+            if (parts == null || parts.isEmpty()) {
+                return;
+            }
+
+            // Convert all parts to lowercase for case-insensitive matching
+            List<String> lowerParts = new ArrayList<>(parts.size());
+            for (String part : parts) {
+                if (part != null && !part.isEmpty()) {
+                    lowerParts.add(part.toLowerCase());
+                }
+            }
+
+            if (lowerParts.isEmpty()) {
+                return;
+            }
+
+            String firstPart = lowerParts.get(0);
+            List<String> remainingParts = lowerParts.size() > 1
+                ? lowerParts.subList(1, lowerParts.size())
+                : Collections.emptyList();
+
+            MultiPartMatch match = new MultiPartMatch(firstPart, 
remainingParts);
+
+            // Always use prefix-based structure, even for short first parts
+            // This ensures multi-part patterns are handled correctly
+            if (firstPart.length() < PREFIX_LENGTH) {
+                // Short first part - use entire first part as prefix for 
grouping
+                String prefix = firstPart; // Use full first part as prefix
+                matchesByPrefix.computeIfAbsent(prefix, k -> {
+                    // Track first character for quick filtering
+                    if (prefix.length() > 0) {
+                        prefixFirstChars.add(prefix.charAt(0));
+                    }
+                    return new ArrayList<>();
+                }).add(match);
+            } else {
+                // Group by prefix of first part
+                String prefix = firstPart.substring(0, PREFIX_LENGTH);
+                matchesByPrefix.computeIfAbsent(prefix, k -> {
+                    // Track first character for quick filtering
+                    prefixFirstChars.add(prefix.charAt(0));
+                    return new ArrayList<>();
+                }).add(match);
+            }
         }
+
+        /**
+         * Check if candidate string contains any of the patterns.
+         * Optimized with character-by-character comparison to avoid substring 
creation.
+         *
+         * Strategy:
+         * 1. First-character filtering: O(1) HashSet lookup skips ~95%+ of 
positions
+         * 2. Character-by-character prefix matching: avoids substring 
allocation
+         * 3. Only check subsequent parts if first part matches (tree pruning)
+         * 4. Early exit on first match
+         *
+         * @param candidateLower Lowercase candidate string to check
+         * @return true if any pattern matches (should be ignored)
+         */
+        boolean containsAny(String candidateLower) {
+            int candidateLen = candidateLower.length();
+            if (candidateLen == 0) {
+                return false;
+            }
+
+            // For prefix-based patterns: check all possible positions
+            // Handle both standard PREFIX_LENGTH prefixes and shorter 
prefixes (for multi-part patterns)
+            int maxCheckPos = candidateLen - 1;
+            if (maxCheckPos < 0) {
+                return false; // Candidate too short
+            }
+
+            // Prefix-based matching with first-character filtering
+            // Strategy: filter by first character to skip most positions, 
then use character-by-character comparison
+            for (int i = 0; i <= maxCheckPos; i++) {
+                char c0 = candidateLower.charAt(i);
+
+                // Quick filter: skip if first character doesn't match any 
prefix
+                if (!prefixFirstChars.contains(c0)) {
+                    continue;
+                }
+
+                // Character-by-character prefix matching to avoid substring 
creation
+                // Try to find matching prefix - check all possible prefix 
lengths
+                List<MultiPartMatch> matchesWithPrefix = null;
+                String matchedPrefix = null;
+                int maxPrefixLen = Math.min(PREFIX_LENGTH, candidateLen - i);
+
+                // Iterate through all prefixes and compare 
character-by-character
+                for (Map.Entry<String, List<MultiPartMatch>> entry : 
matchesByPrefix.entrySet()) {
+                    String prefix = entry.getKey();
+                    int prefixLen = prefix.length();
+
+                    // Skip if prefix doesn't start with matching character or 
is too long
+                    if (prefixLen > maxPrefixLen || prefix.charAt(0) != c0) {
+                        continue;
+                    }
+
+                    // Check if we have enough characters remaining
+                    if (i + prefixLen > candidateLen) {
+                        continue;
+                    }
+
+                    // Character-by-character comparison (avoids substring 
creation)
+                    boolean prefixMatches = true;
+                    for (int j = 1; j < prefixLen; j++) {
+                        if (candidateLower.charAt(i + j) != prefix.charAt(j)) {
+                            prefixMatches = false;
+                            break;
+                        }
+                    }
+
+                    if (prefixMatches) {
+                        matchesWithPrefix = entry.getValue();
+                        matchedPrefix = prefix;
+                        break; // Found match, no need to check others
+                    }
+                }
+
+                if (matchesWithPrefix != null && matchedPrefix != null) {
+                    int prefixLen = matchedPrefix.length();
+                    // Prefix matches - check multi-part matches (only this 
subset)
+                    for (MultiPartMatch match : matchesWithPrefix) {
+                        // Find first part - prefix matches at position i, so 
pattern could start at i or before
+                        int patternLen = match.firstPart.length();
+                        int firstPartPos = -1;
+
+                        // Fast path: check if pattern starts at position i 
(most common case)
+                        // Since prefix is at the start of pattern, pattern 
most likely starts at i
+                        if (i + patternLen <= candidateLen) {
+                            boolean matchesAtI = true;
+                            // Only need to check characters after the prefix 
(already matched)
+                            int checkStart = Math.min(prefixLen, patternLen);
+                            for (int j = checkStart; j < patternLen; j++) {
+                                if (candidateLower.charAt(i + j) != 
match.firstPart.charAt(j)) {
+                                    matchesAtI = false;
+                                    break;
+                                }
+                            }
+                            if (matchesAtI) {
+                                firstPartPos = i;
+                            }
+                        }
+
+                        // If fast path didn't match, use indexOf to search 
backwards
+                        // (pattern could start before i if prefix appears 
elsewhere in pattern)
+                        if (firstPartPos < 0) {
+                            int searchStart = Math.max(0, i - patternLen + 
Math.min(patternLen, PREFIX_LENGTH));
+                            firstPartPos = 
candidateLower.indexOf(match.firstPart, searchStart);
+                            // Pattern can't start after position i (prefix is 
at start of pattern)
+                            if (firstPartPos > i) {
+                                firstPartPos = -1;
+                            }
+                        }
+
+                        if (firstPartPos >= 0) {
+                            // First part found - now check remaining parts in 
sequence
+                            if (match.remainingParts.isEmpty()) {
+                                // Single-part match - we're done
+                                return true;
+                            }
+
+                            // Check remaining parts appear in order after 
first part
+                            int currentPos = firstPartPos + patternLen;
+                            boolean allPartsMatch = true;
+
+                            for (String remainingPart : match.remainingParts) {
+                                int nextPos = 
candidateLower.indexOf(remainingPart, currentPos);
+                                if (nextPos < 0) {
+                                    // This part not found after previous part 
- prune this branch
+                                    allPartsMatch = false;
+                                    break;
+                                }
+                                // Move position forward for next part
+                                currentPos = nextPos + remainingPart.length();
+                            }
+
+                            if (allPartsMatch) {
+                                return true; // All parts matched in sequence
+                            }
+                        }
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        /**
+         * Check if any patterns are configured
+         */
+        boolean isEmpty() {
+            return matchesByPrefix.isEmpty();
+        }
+    }
+
+    /**
+     * Create a builder for configuring LogChecker with specific patterns.
+     * This is the recommended way to create LogChecker instances for better 
performance.
+     *
+     * Example:
+     * <pre>
+     * LogChecker checker = LogChecker.builder()
+     *     .addIgnoredSubstring("Response status code: 400")                // 
Single substring
+     *     .addIgnoredMultiPart("Schema", "not found")                     // 
Multi-part: sequential matching
+     *     .build();
+     * </pre>
+     *
+     * IMPORTANT: All substrings are literal (no regex). Uses hierarchical 
prefix-based matching with
+     * tree structure. Multi-part patterns only check subsequent parts if 
first part matches.
+     *
+     * @return A LogCheckerBuilder instance
+     */
+    public static LogCheckerBuilder builder() {
+        return new LogCheckerBuilder();
     }
-    
+
     /**
-     * Check if a pattern is a literal string (no regex special characters)
+     * Builder for creating LogChecker instances with specific substrings to 
ignore.
+     * This allows tests to only add the substrings they need, significantly 
improving performance.
      */
-    private boolean isLiteralPattern(String pattern) {
-        // Check for regex special characters (excluding . which is common in 
literal strings)
-        // We consider it literal if it only contains alphanumeric, spaces, 
and common punctuation
-        for (int i = 0; i < pattern.length(); i++) {
-            char c = pattern.charAt(i);
-            if (c == '*' || c == '+' || c == '?' || c == '^' || c == '$' || 
-                c == '[' || c == ']' || c == '{' || c == '}' || c == '(' || c 
== ')' ||
-                c == '|' || c == '\\') {
-                return false;
+    public static class LogCheckerBuilder {
+        private int errorContextLinesBefore = 10;
+        private int errorContextLinesAfter = 10;
+        private int warningContextLinesBefore = 0;
+        private int warningContextLinesAfter = 0;
+        private final List<Object> substrings = new ArrayList<>(); // Can be 
String or MultiPartSubstring
+
+        /**
+         * Set context lines for errors
+         */
+        public LogCheckerBuilder withErrorContext(int before, int after) {
+            this.errorContextLinesBefore = before;
+            this.errorContextLinesAfter = after;
+            return this;
+        }
+
+        /**
+         * Set context lines for warnings
+         */
+        public LogCheckerBuilder withWarningContext(int before, int after) {
+            this.warningContextLinesBefore = before;
+            this.warningContextLinesAfter = after;
+            return this;
+        }
+
+        /**
+         * Add a single substring to ignore.
+         *
+         * @param substring Literal substring to match (case-insensitive)
+         * @return This builder for method chaining
+         */
+        public LogCheckerBuilder addIgnoredSubstring(String substring) {
+            this.substrings.add(substring);
+            return this;
+        }
+
+        /**
+         * Add a multi-part substring pattern (substrings must appear in 
sequence).
+         * This allows matching complex patterns without regex.
+         *
+         * Example: addIgnoredMultiPart("Schema", "not found") matches 
"Schema" followed by "not found"
+         *
+         * @param parts Substrings that must appear in order
+         * @return This builder for method chaining
+         */
+        public LogCheckerBuilder addIgnoredMultiPart(String... parts) {
+            if (parts != null && parts.length > 0) {
+                this.substrings.add(new 
MultiPartSubstring(Arrays.asList(parts)));
             }
+            return this;
+        }
+
+        /**
+         * Add multiple substrings to ignore
+         *
+         * @param substrings Array of substrings to add
+         * @return This builder for method chaining
+         */
+        public LogCheckerBuilder addIgnoredSubstrings(String... substrings) {
+            Collections.addAll(this.substrings, substrings);
+            return this;
+        }
+
+        /**
+         * Add multiple substrings to ignore
+         *
+         * @param substrings List of substrings to add
+         * @return This builder for method chaining
+         */
+        public LogCheckerBuilder addIgnoredSubstrings(List<String> substrings) 
{
+            if (substrings != null) {
+                this.substrings.addAll(substrings);
+            }
+            return this;
+        }
+
+        /**
+         * Marker class to distinguish multi-part substrings from single 
substrings
+         */
+        private static class MultiPartSubstring {
+            final List<String> parts;
+            MultiPartSubstring(List<String> parts) {
+                this.parts = parts;
+            }
+        }
+
+        /**
+         * Build the LogChecker instance
+         */
+        public LogChecker build() {
+            LogChecker checker = new LogChecker(
+                errorContextLinesBefore, errorContextLinesAfter,
+                warningContextLinesBefore, warningContextLinesAfter
+            );
+            // Add all substrings specified by the builder
+            for (Object substring : substrings) {
+                if (substring instanceof MultiPartSubstring) {
+                    checker.addIgnoredMultiPart(((MultiPartSubstring) 
substring).parts);
+                } else if (substring instanceof String) {
+                    checker.addIgnoredSubstring((String) substring);
+                }
+            }
+            return checker;
         }
-        return true;
     }
-    
+
     /**
-     * Optimize regex pattern to prevent catastrophic backtracking
-     * - Replace .* with .*+ (possessive quantifier) to prevent backtracking
-     * - This makes greedy matching non-backtracking, which is safe for 
"contains" matching
+     * Add a single literal substring to ignore (expected errors/warnings).
+     *
+     * @param substring Literal substring to match against log messages 
(case-insensitive)
+     *
+     * IMPORTANT: All substrings are literal (no regex). This uses fast 
hierarchical prefix-based matching
+     * for optimal performance.
      */
-    private String optimizePattern(String pattern) {
-        // Replace .* with .*+ (possessive quantifier) to prevent backtracking
-        // This is safe because we're using find(), not matches(), so we just 
need to find the pattern anywhere
-        // Possessive quantifiers prevent backtracking which can cause 
exponential time complexity
-        return pattern.replace(".*", ".*+");
+    public void addIgnoredSubstring(String substring) {
+        if (substring != null && !substring.isEmpty()) {
+            literalSubstringMatcher.addPattern(substring);
+        }
     }
-    
+
     /**
-     * Add multiple patterns to ignore
-     * @param patterns List of regex patterns
+     * Add a multi-part substring pattern to ignore (substrings must appear in 
sequence).
+     * This allows matching complex patterns without regex or backtracking.
+     *
+     * Example: addIgnoredMultiPart("Schema", "not found") will match "Schema" 
followed by "not found"
+     * anywhere in the log message, but only checks "not found" if "Schema" is 
found first.
+     *
+     * @param parts List of substrings that must appear in order 
(case-insensitive)
      */
-    public void addIgnoredPatterns(List<String> patterns) {
-        if (patterns != null) {
-            for (String pattern : patterns) {
-                addIgnoredPattern(pattern);
-            }
+    public void addIgnoredMultiPart(List<String> parts) {
+        if (parts != null && !parts.isEmpty()) {
+            literalSubstringMatcher.addMultiPartPattern(parts);
         }
     }
-    
+
     /**
-     * Add default ignored patterns for common expected errors
+     * Add a multi-part substring pattern to ignore (substrings must appear in 
sequence).
+     *
+     * @param parts Array of substrings that must appear in order 
(case-insensitive)
      */
-    private void addDefaultIgnoredPatterns() {
-        // BundleWatcher warnings (common during startup)
-        addIgnoredPattern("BundleWatcher.*WARN");
-        // Old-style feature file deprecation warnings
-        addIgnoredPattern("DEPRECATED.*feature.*file");
-        // Segment condition recommendations
-        addIgnoredPattern("segment.*condition.*recommendation");
-        // KarafTestWatcher FAILED messages (just echoes of test failures)
-        addIgnoredPattern("KarafTestWatcher.*FAILED:");
-        // Dynamic test conditions (expected during test runs)
-        addIgnoredPattern("loginEventCondition for rule testLogin");
-        // Deprecated legacy query builder warnings
-        addIgnoredPattern("DEPRECATED.*Using legacy queryBuilderId");
-        // Test migration script intentional failures (expected in migration 
recovery tests)
-        addIgnoredPattern("failingMigration.*Intentional failure");
-        addIgnoredPattern("Error executing migration 
script.*failingMigration");
-        
-        // InvalidRequestExceptionMapper errors (expected in InputValidationIT 
tests)
-        addIgnoredPattern("InvalidRequestExceptionMapper.*Invalid parameter");
-        addIgnoredPattern("InvalidRequestExceptionMapper.*Invalid Context 
request object");
-        addIgnoredPattern("InvalidRequestExceptionMapper.*Invalid events 
collector object");
-        addIgnoredPattern("InvalidRequestExceptionMapper.*Invalid profile ID 
format in cookie");
-        addIgnoredPattern("InvalidRequestExceptionMapper.*events collector 
cannot be empty");
-        addIgnoredPattern("InvalidRequestExceptionMapper.*Unable to 
deserialize object because");
-        addIgnoredPattern("InvalidRequestExceptionMapper.*Incoming POST 
request blocked because exceeding maximum bytes size");
-        // RequestValidatorInterceptor warnings (expected when testing request 
size limits)
-        addIgnoredPattern("RequestValidatorInterceptor.*Incoming POST request 
blocked because exceeding maximum bytes size");
-        addIgnoredPattern("RequestValidatorInterceptor.*has thrown exception, 
unwinding now");
-        addIgnoredPattern("RequestValidatorInterceptor.*Interceptor for.*has 
thrown exception, unwinding now");
-        
addIgnoredPattern("org\\.apache\\.unomi\\.rest\\.validation\\.request\\.RequestValidatorInterceptor.*has
 thrown exception");
-        addIgnoredPattern("InvalidRequestException.*Incoming POST request 
blocked because exceeding maximum bytes size");
-        addIgnoredPattern(".*Incoming POST request blocked because exceeding 
maximum bytes size allowed on: /cxs/eventcollector");
-        // More general patterns that match regardless of logger name format
-        addIgnoredPattern(".*has thrown exception, unwinding now.*Incoming 
POST request blocked because exceeding maximum bytes size");
-        addIgnoredPattern(".*Interceptor for.*has thrown exception, unwinding 
now.*exceeding maximum bytes size");
-        addIgnoredPattern(".*exceeding maximum bytes size allowed on: 
/cxs/eventcollector.*limit: 200000");
-        addIgnoredPattern(".*exceeding maximum bytes size.*limit: 
200000.*request size: 210940");
-        // Match the exact error message format from the exception (with 
escaped parentheses)
-        addIgnoredPattern(".*Incoming POST request blocked because exceeding 
maximum bytes size allowed on: /cxs/eventcollector \\(limit: 200000, request 
size: 210940\\)");
-        // Very general pattern that matches the key error message parts
-        addIgnoredPattern(".*blocked because exceeding maximum bytes 
size.*/cxs/eventcollector");
-        // Simple pattern matching just the key error message
-        addIgnoredPattern(".*exceeding maximum bytes size allowed on: 
/cxs/eventcollector");
-        // Very simple patterns that match the core error message parts
-        addIgnoredPattern(".*exceeding maximum bytes 
size.*/cxs/eventcollector.*limit: 200000");
-        addIgnoredPattern(".*blocked because exceeding maximum bytes size");
-        // Match if we see both key parts anywhere in the log entry
-        addIgnoredPattern(".*has thrown exception.*exceeding maximum bytes 
size");
-        addIgnoredPattern(".*TestEndPoint.*exceeding maximum bytes size");
-        
-        // Test-related schema errors (expected in JSONSchemaIT and other 
tests)
-        addIgnoredPattern("Schema not found for event type: dummy");
-        addIgnoredPattern("Schema not found for event type: flattened");
-        addIgnoredPattern("Error executing system operation: Test exception");
-        addIgnoredPattern("Error executing system 
operation:.*ValidationException.*Schema not found");
-        addIgnoredPattern("Couldn't find persona");
-        addIgnoredPattern("Unable to save schema");
-        addIgnoredPattern("SchemaServiceImpl.*Couldn't find schema");
-        addIgnoredPattern("JsonSchemaFactory.*Failed to load json schema");
-        addIgnoredPattern("Failed to load json schema!");
-        addIgnoredPattern("Couldn't find schema.*vendor.test.com");
-        addIgnoredPattern("JsonSchemaException.*Couldn't find schema");
-        addIgnoredPattern("InvocationTargetException.*JsonSchemaException");
-        addIgnoredPattern("InvocationTargetException.*Couldn't find schema");
-        
addIgnoredPattern(".*InvocationTargetException.*JsonSchemaException.*Couldn't 
find schema");
-        addIgnoredPattern("IOException.*Couldn't find schema");
-        addIgnoredPattern("ValidatorTypeCode.*InvocationTargetException");
-        
addIgnoredPattern("ValidatorTypeCode.*Error:.*InvocationTargetException");
-        // Schema validation warnings (expected during schema validation tests)
-        addIgnoredPattern("SchemaServiceImpl.*Schema validation found.*errors 
while validating");
-        addIgnoredPattern("SchemaServiceImpl.*Validation error.*does not match 
the regex pattern");
-        addIgnoredPattern("SchemaServiceImpl.*An error occurred during the 
validation of your event");
-        addIgnoredPattern("SchemaServiceImpl.*Validation error: There are 
unevaluated properties");
-        addIgnoredPattern("SchemaServiceImpl.*Validation error: Unknown scope 
value");
-        addIgnoredPattern("SchemaServiceImpl.*Validation error:.*may only have 
a maximum of.*properties");
-        addIgnoredPattern("SchemaServiceImpl.*Validation error:.*string found, 
number expected");
-        
-        // Action type resolution warnings (expected in tests with missing 
action types)
-        addIgnoredPattern("ParserHelper.*Couldn't resolve action type");
-        addIgnoredPattern("ResolverServiceImpl.*Marked rules.*as invalid: 
Unresolved action type");
-        
-        // Test-related property copy errors (expected in 
CopyPropertiesActionIT)
-        addIgnoredPattern("Impossible to copy the property");
-        
-        // Expected HTTP response codes in tests
-        addIgnoredPattern("Response status code: 204");
-        addIgnoredPattern("Response status code: 400");
-        
-        // Shutdown-related errors (expected during test teardown)
-        addIgnoredPattern("FrameworkEvent ERROR");
-        addIgnoredPattern("EventDispatcher: Error during dispatch.*Blueprint 
container is being or has been destroyed");
-        
-        // Test query errors (expected when testing invalid queries/scroll IDs)
-        addIgnoredPattern("Error while executing in class 
loader.*scrollIdentifier=dummyScrollId");
-        addIgnoredPattern("Error while executing in class loader.*Error 
loading itemType");
-        addIgnoredPattern("Error while executing in class loader.*Error 
continuing scrolling query");
-        addIgnoredPattern("OpenSearchPersistenceServiceImpl.*Error while 
executing in class loader");
-        addIgnoredPattern("OpenSearchPersistenceServiceImpl\\.\\d+.*Error 
while executing in class loader");
-        addIgnoredPattern("OpenSearchPersistenceServiceImpl\\.\\d+:\\d+.*Error 
while executing in class loader");
-        addIgnoredPattern("ElasticSearchPersistenceServiceImpl.*Error while 
executing in class loader");
-        addIgnoredPattern("Error continuing scrolling 
query.*scrollIdentifier=dummyScrollId");
-        addIgnoredPattern(".*Error continuing scrolling query for 
itemType=org\\.apache\\.unomi\\.api\\.Profile.*scrollIdentifier=dummyScrollId");
-        addIgnoredPattern("Error continuing scrolling query for 
itemType.*scrollIdentifier=dummyScrollId");
-        addIgnoredPattern("java\\.lang\\.Exception.*Error continuing scrolling 
query.*scrollIdentifier=dummyScrollId");
-        addIgnoredPattern("Cannot parse scroll id");
-        addIgnoredPattern("Request failed:.*illegal_argument_exception.*Cannot 
parse scroll id");
-        
-        // Index and mapping errors (expected during test setup/teardown or 
when testing mapping scenarios)
-        addIgnoredPattern("Could not find index.*could not register item 
type");
-        addIgnoredPattern("mapper_parsing_exception.*tried to parse field.*as 
object, but found a concrete value");
-        
-        // Condition validation errors (expected in tests with invalid 
conditions)
-        addIgnoredPattern("Failed to validate condition");
-        addIgnoredPattern("Error executing condition 
evaluator.*pastEventConditionEvaluator");
+    public void addIgnoredMultiPart(String... parts) {
+        if (parts != null && parts.length > 0) {
+            literalSubstringMatcher.addMultiPartPattern(Arrays.asList(parts));
+        }
     }
-    
+
+    /**
+     * Add multiple substrings to ignore
+     * @param substrings List of literal substrings
+     */
+    public void addIgnoredSubstrings(List<String> substrings) {
+        if (substrings != null) {
+            for (String substring : substrings) {
+                addIgnoredSubstring(substring);
+            }
+        }
+    }
+
     /**
      * Mark the current log position as the starting point for the next check
      */
     public void markCheckpoint() {
         checkpointIndex = InMemoryLogAppender.getEventCount();
     }
-    
+
     /**
      * Check logs since the last checkpoint for errors and warnings
      * @return LogCheckResult containing any errors/warnings found
@@ -466,7 +740,7 @@ public class LogChecker {
         List<Object> events = getEventsSince(checkpointIndex);
         return processEvents(events, checkpointIndex);
     }
-    
+
     /**
      * Get events since checkpoint using reflection to avoid direct LogEvent 
dependency
      * Converts List<LogEvent> to List<Object> by copying elements
@@ -479,7 +753,7 @@ public class LogChecker {
             if (eventsList == null) {
                 return Collections.emptyList();
             }
-            
+
             // Create a new ArrayList<Object> and copy all elements
             List<Object> result = new ArrayList<>();
             if (eventsList instanceof List) {
@@ -495,7 +769,7 @@ public class LogChecker {
             return Collections.emptyList();
         }
     }
-    
+
     /**
      * Process log events and extract errors/warnings with context
      * Uses reflection to extract data from LogEvent objects without importing 
Log4j2 core classes
@@ -503,47 +777,47 @@ public class LogChecker {
     private LogCheckResult processEvents(List<Object> events, int baseIndex) {
         List<LogEntry> errors = new ArrayList<>();
         List<LogEntry> warnings = new ArrayList<>();
-        
+
         for (int i = 0; i < events.size(); i++) {
             Object event = events.get(i);
             EventData eventData = extractEventData(event);
-            
+
             if (eventData == null) {
                 continue;
             }
-            
+
             // Only process ERROR, WARN, and FATAL levels
             if (isErrorOrWarningLevel(eventData.level)) {
                 LogEntry entry = createLogEntry(eventData, baseIndex + i + 1);
-                
+
                 if (shouldIncludeEntry(entry)) {
                     // Determine context lengths based on log level
                     boolean isError = isErrorLevel(eventData.level);
                     int contextBefore = isError ? errorContextLinesBefore : 
warningContextLinesBefore;
                     int contextAfter = isError ? errorContextLinesAfter : 
warningContextLinesAfter;
-                    
+
                     // Capture context before
                     int startBefore = Math.max(0, i - contextBefore);
                     for (int j = startBefore; j < i; j++) {
                         EventData contextData = 
extractEventData(events.get(j));
                         if (contextData != null) {
                             entry.addContextBefore(new ContextEvent(
-                                contextData.timestamp, contextData.level, 
+                                contextData.timestamp, contextData.level,
                                 contextData.thread, contextData.logger, 
contextData.message));
                         }
                     }
-                    
+
                     // Capture context after
                     int endAfter = Math.min(events.size(), i + 1 + 
contextAfter);
                     for (int j = i + 1; j < endAfter; j++) {
                         EventData contextData = 
extractEventData(events.get(j));
                         if (contextData != null) {
                             entry.addContextAfter(new ContextEvent(
-                                contextData.timestamp, contextData.level, 
+                                contextData.timestamp, contextData.level,
                                 contextData.thread, contextData.logger, 
contextData.message));
                         }
                     }
-                    
+
                     // Add stack trace if present
                     if (eventData.throwable != null) {
                         String[] stackTrace = 
getStackTrace(eventData.throwable);
@@ -551,15 +825,15 @@ public class LogChecker {
                             entry.addStacktraceLine(line);
                         }
                     }
-                    
+
                     addEntryToResults(entry, errors, warnings);
                 }
             }
         }
-        
+
         return new LogCheckResult(errors, warnings);
     }
-    
+
     /**
      * Data extracted from a LogEvent (avoids storing LogEvent directly)
      */
@@ -570,7 +844,7 @@ public class LogChecker {
         final String logger;
         final String message;
         final Throwable throwable;
-        
+
         EventData(String timestamp, String level, String thread, String 
logger, String message, Throwable throwable) {
             this.timestamp = timestamp;
             this.level = level;
@@ -580,7 +854,7 @@ public class LogChecker {
             this.throwable = throwable;
         }
     }
-    
+
     /**
      * Extract data from a LogEvent using reflection to avoid direct dependency
      */
@@ -588,23 +862,23 @@ public class LogChecker {
         try {
             // Use reflection to access LogEvent methods without importing the 
class
             Class<?> eventClass = event.getClass();
-            
+
             // Get level
             Object levelObj = eventClass.getMethod("getLevel").invoke(event);
             String level = levelObj != null ? levelObj.toString() : "UNKNOWN";
-            
+
             // Get instant/timestamp and format it
             Object instantObj = 
eventClass.getMethod("getInstant").invoke(event);
             String timestamp = formatInstant(instantObj);
-            
+
             // Get thread name
             String thread = (String) 
eventClass.getMethod("getThreadName").invoke(event);
             if (thread == null) thread = "";
-            
+
             // Get logger name
             String logger = (String) 
eventClass.getMethod("getLoggerName").invoke(event);
             if (logger == null) logger = "";
-            
+
             // Get message
             Object messageObj = 
eventClass.getMethod("getMessage").invoke(event);
             String message = "";
@@ -614,10 +888,10 @@ public class LogChecker {
                     message = formattedMsg.toString();
                 }
             }
-            
+
             // Get throwable
             Throwable throwable = (Throwable) 
eventClass.getMethod("getThrown").invoke(event);
-            
+
             return new EventData(timestamp, level, thread, logger, message, 
throwable);
         } catch (Exception e) {
             // Use System.err to avoid creating logs that would be captured by 
InMemoryLogAppender
@@ -626,22 +900,22 @@ public class LogChecker {
             return null;
         }
     }
-    
+
     /**
      * Check if level is ERROR, WARN, or FATAL
      */
     private boolean isErrorOrWarningLevel(String level) {
         return "ERROR".equals(level) || "WARN".equals(level) || 
"FATAL".equals(level);
     }
-    
+
     /**
      * Create a LogEntry from extracted event data
      */
     private LogEntry createLogEntry(EventData eventData, long lineNumber) {
-        return new LogEntry(eventData.timestamp, eventData.level, 
eventData.thread, 
+        return new LogEntry(eventData.timestamp, eventData.level, 
eventData.thread,
                            eventData.logger, eventData.message, lineNumber);
     }
-    
+
     /**
      * Get stack trace as array of strings
      */
@@ -654,7 +928,7 @@ public class LogChecker {
         throwable.printStackTrace(pw);
         return sw.toString().split("\n");
     }
-    
+
     /**
      * Add a log entry to the appropriate result list (errors or warnings)
      */
@@ -666,61 +940,65 @@ public class LogChecker {
             warnings.add(entry);
         }
     }
-    
+
     /**
      * Check if a log level represents an error
      */
     private boolean isErrorLevel(String level) {
         return "ERROR".equals(level) || "FATAL".equals(level);
     }
-    
+
     /**
      * Check if a log entry should be included (not ignored)
+     *
+     * CRITICAL PERFORMANCE: This method is called for every ERROR/WARN/FATAL 
log entry (43,000+).
+     * Optimized for minimal operations and single-pass processing:
+     * - Early exit if no patterns configured
+     * - Avoids expensive operations (getFullMessage, toLowerCase) unless 
needed
+     * - Single-pass string building with length limit
+     * - Early exit on first substring match
+     * - No regex: uses fast hierarchical prefix-based matching
+     *
+     * Package-private for testing purposes.
      */
-    private boolean shouldIncludeEntry(LogEntry entry) {
-        // Default ignores based on level/logger (fast path)
+    boolean shouldIncludeEntry(LogEntry entry) {
+        // Fast path: default ignores based on level/logger (no string 
building needed)
         if ("WARN".equals(entry.getLevel()) && entry.getLogger() != null && 
entry.getLogger().contains("BundleWatcher")) {
             return false;
         }
-        
-        // Build a rich candidate string for matching custom ignored patterns
-        StringBuilder candidateBuilder = new StringBuilder();
-        candidateBuilder.append(entry.getLevel() != null ? entry.getLevel() : 
"")
-                .append(' ')
-                .append(entry.getLogger() != null ? entry.getLogger() : "")
-                .append(' ')
-                .append(entry.getMessage() != null ? entry.getMessage() : "")
-                .append(' ')
-                .append(entry.getFullMessage() != null ? 
entry.getFullMessage() : "");
+
+        // Early exit: if no substrings configured, include all entries
+        if (literalSubstringMatcher.isEmpty()) {
+            return true;
+        }
+
+        // Build candidate string in single pass with length limit
+        // Prefer message over fullMessage (which includes stack trace) for 
performance
+        String level = entry.getLevel() != null ? entry.getLevel() : "";
+        String logger = entry.getLogger() != null ? entry.getLogger() : "";
+        String message = entry.getMessage() != null ? entry.getMessage() : "";
+
+        // Build candidate: level + logger + message (most common case)
+        // No need to include fullMessage since we only use literal substrings
+        StringBuilder candidateBuilder = new 
StringBuilder(Math.min(level.length() + logger.length() + message.length() + 
10, MAX_CANDIDATE_LENGTH));
+        candidateBuilder.append(level).append(' ').append(logger).append(' 
').append(message);
+
+        // Ensure we don't exceed the limit (safety check)
         String candidate = candidateBuilder.toString();
-        String candidateLower = candidate.toLowerCase(); // For 
case-insensitive literal matching
-        
-        // Fast path: check literal patterns first (much faster than regex)
-        for (String literal : literalPatterns) {
-            if (candidateLower.contains(literal)) {
-                return false;
-            }
+        if (candidate.length() > MAX_CANDIDATE_LENGTH) {
+            candidate = candidate.substring(0, MAX_CANDIDATE_LENGTH);
         }
-        
-        // Slower path: check regex patterns (reuse cached matchers)
-        for (Pattern pattern : ignoredPatterns) {
-            java.util.regex.Matcher matcher = matcherCache.get(pattern);
-            if (matcher != null) {
-                // Reset the matcher with the new candidate string (reuses the 
Matcher object)
-                matcher.reset(candidate);
-                if (matcher.find()) {
-                    return false;
-                }
-            } else {
-                // Fallback if matcher not in cache (shouldn't happen, but be 
safe)
-                if (pattern.matcher(candidate).find()) {
-                    return false;
-                }
-            }
+
+        // Check literal substrings using hierarchical prefix-based matching
+        // This minimizes character comparisons by checking prefixes first
+        String candidateLower = candidate.toLowerCase();
+        if (literalSubstringMatcher.containsAny(candidateLower)) {
+            return false; // Early exit on first match
         }
+
         return true;
     }
-    
+
     /**
      * Format an Instant object to a compact timecode (HH:mm:ss.SSS)
      */
@@ -730,7 +1008,7 @@ public class LogChecker {
         }
         try {
             Instant instant = null;
-            
+
             // If it's already an Instant, use it directly
             if (instantObj instanceof Instant) {
                 instant = (Instant) instantObj;
@@ -756,7 +1034,7 @@ public class LogChecker {
                     Pattern nanoPattern = Pattern.compile("nano=(\\d+)");
                     java.util.regex.Matcher epochMatcher = 
epochPattern.matcher(instantStr);
                     java.util.regex.Matcher nanoMatcher = 
nanoPattern.matcher(instantStr);
-                    
+
                     if (epochMatcher.find()) {
                         long epochSeconds = 
Long.parseLong(epochMatcher.group(1));
                         long nanos = 0;
@@ -767,13 +1045,13 @@ public class LogChecker {
                     }
                 }
             }
-            
+
             if (instant != null) {
                 // Format as compact timecode: HH:mm:ss.SSS
                 return DateTimeFormatter.ofPattern("HH:mm:ss.SSS")
                     .format(instant.atZone(ZoneId.systemDefault()));
             }
-            
+
             // Fallback to original string if we can't parse it
             return instantObj.toString();
         } catch (Exception e) {
@@ -781,7 +1059,7 @@ public class LogChecker {
             return instantObj.toString();
         }
     }
-    
+
     /**
      * Format a timestamp string (already extracted) to compact timecode 
format (HH:mm:ss.SSS)
      * This is only called for ContextEvent timestamps which are already 
strings from formatInstant()
@@ -801,7 +1079,7 @@ public class LogChecker {
                 Pattern nanoPattern = Pattern.compile("nano=(\\d+)");
                 java.util.regex.Matcher epochMatcher = 
epochPattern.matcher(timestamp);
                 java.util.regex.Matcher nanoMatcher = 
nanoPattern.matcher(timestamp);
-                
+
                 if (epochMatcher.find()) {
                     long epochSeconds = Long.parseLong(epochMatcher.group(1));
                     long nanos = 0;
@@ -818,7 +1096,7 @@ public class LogChecker {
         // Return as-is for any other format
         return timestamp;
     }
-    
+
     /**
      * Shorten logger name to just the class name (remove package)
      */
@@ -832,7 +1110,7 @@ public class LogChecker {
         }
         return logger;
     }
-    
+
     /**
      * Shorten thread name for compact display (keep last part if it contains 
useful info)
      */
@@ -848,7 +1126,7 @@ public class LogChecker {
         }
         return thread;
     }
-    
+
     /**
      * Truncate message if it's too long
      */
@@ -861,7 +1139,7 @@ public class LogChecker {
         }
         return message.substring(0, maxLength - 3) + "...";
     }
-    
+
     /**
      * Extract source location (class:line) from stack trace, skipping logging 
framework classes
      */
@@ -869,47 +1147,47 @@ public class LogChecker {
         if (stacktrace == null || stacktrace.isEmpty()) {
             return null;
         }
-        
+
         // Patterns to skip (logging framework classes)
         Pattern skipPattern = Pattern.compile(
             
".*(org\\.apache\\.logging|org\\.slf4j|ch\\.qos\\.logback|org\\.log4j|" +
             "java\\.util\\.logging|sun\\.reflect|jdk\\.internal\\.reflect).*"
         );
-        
+
         // Pattern to match stack trace lines: at 
package.ClassName.methodName(FileName.java:lineNumber)
         // Group 1: full qualified name (package.ClassName.methodName)
         // Group 2: line number
         Pattern stackTracePattern = Pattern.compile(
             "\\s*at\\s+([\\w.$<>]+)\\([\\w.]+\\.java:(\\d+)\\)"
         );
-        
+
         for (String line : stacktrace) {
             if (line == null || line.trim().isEmpty()) {
                 continue;
             }
-            
+
             // Skip logging framework classes
             if (skipPattern.matcher(line).matches()) {
                 continue;
             }
-            
+
             // Try to match stack trace pattern
             java.util.regex.Matcher matcher = stackTracePattern.matcher(line);
             if (matcher.find()) {
                 String fullQualifiedName = matcher.group(1);
                 String lineNumber = matcher.group(2);
-                
+
                 // Extract class name from full qualified name 
(package.ClassName.methodName)
                 // Remove method name by finding the last dot before method 
name
                 // For inner classes, we want the outer class name
                 String className = fullQualifiedName;
-                
+
                 // Remove generic type parameters if present
                 int genericStart = className.indexOf('<');
                 if (genericStart > 0) {
                     className = className.substring(0, genericStart);
                 }
-                
+
                 // Extract class name (everything up to the last dot before 
method name)
                 // Method names typically start with lowercase, but we'll use 
a simpler approach:
                 // Take the part before the last dot that contains the class
@@ -918,25 +1196,25 @@ public class LogChecker {
                     // Check if the part after last dot looks like a method 
(starts with lowercase or is a common method pattern)
                     String afterDot = className.substring(lastDot + 1);
                     // If it's all uppercase or contains $, it might be a 
class, otherwise assume it's a method
-                    if (afterDot.length() > 0 && 
Character.isLowerCase(afterDot.charAt(0)) && 
+                    if (afterDot.length() > 0 && 
Character.isLowerCase(afterDot.charAt(0)) &&
                         !afterDot.contains("$")) {
                         // Likely a method name, get the class name before it
                         className = className.substring(0, lastDot);
                     }
                 }
-                
+
                 // Extract just the simple class name (last part)
                 lastDot = className.lastIndexOf('.');
                 String simpleClassName = (lastDot >= 0) ? 
className.substring(lastDot + 1) : className;
-                
+
                 // Remove inner class markers ($)
                 simpleClassName = simpleClassName.replace('$', '.');
-                
+
                 // Return compact format: ClassName:lineNumber
                 return simpleClassName + ":" + lineNumber;
             }
         }
-        
+
         return null;
     }
 }
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/tools/LogCheckerTest.java 
b/itests/src/test/java/org/apache/unomi/itests/tools/LogCheckerTest.java
new file mode 100644
index 000000000..6fcd48c86
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/tools/LogCheckerTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.unomi.itests.tools;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Comprehensive unit tests for LogChecker substring matching functionality.
+ * Tests validate the hierarchical prefix-based matching algorithm, multi-part 
substring matching,
+ * edge cases, and performance characteristics.
+ */
+public class LogCheckerTest {
+
+    private LogChecker logChecker;
+
+    @Before
+    public void setUp() {
+        logChecker = LogChecker.builder()
+            .withErrorContext(0, 0)
+            .withWarningContext(0, 0)
+            .build();
+    }
+
+    @Test
+    public void testSingleSubstringMatch() {
+        logChecker.addIgnoredSubstring("error occurred");
+
+        assertFalse("Should ignore message with substring", shouldInclude("An 
error occurred in the system"));
+        assertTrue("Should include message without substring", 
shouldInclude("This is a normal log message"));
+    }
+
+    @Test
+    public void testSingleSubstringCaseInsensitive() {
+        logChecker.addIgnoredSubstring("ERROR OCCURRED");
+
+        assertFalse("Should match case-insensitively", shouldInclude("An error 
occurred in the system"));
+        assertFalse("Should match case-insensitively", shouldInclude("An ERROR 
OCCURRED in the system"));
+    }
+
+    @Test
+    public void testMultiPartSubstringMatch() {
+        logChecker.addIgnoredMultiPart("Schema", "not found");
+
+        assertFalse("Should match multi-part in sequence", 
shouldInclude("Schema not found for event type"));
+        assertFalse("Should match with text between parts", 
shouldInclude("Schema validation not found"));
+        assertTrue("Should not match if second part missing", 
shouldInclude("Schema validation found"));
+        assertTrue("Should not match if order is wrong", shouldInclude("not 
found Schema"));
+    }
+
+    @Test
+    public void testMultiPartSubstringThreeParts() {
+        logChecker.addIgnoredMultiPart("Invalid", "parameter", "format");
+
+        assertFalse("Should match all three parts in order", 
shouldInclude("Invalid parameter format detected"));
+        assertFalse("Should match with text between", shouldInclude("Invalid 
request parameter format error"));
+        assertTrue("Should not match if third part missing", 
shouldInclude("Invalid parameter"));
+        assertTrue("Should not match if order is wrong", 
shouldInclude("Invalid format parameter"));
+    }
+
+    @Test
+    public void testMultipleSubstrings() {
+        logChecker.addIgnoredSubstring("specific error");
+        logChecker.addIgnoredSubstring("warning message");
+        logChecker.addIgnoredMultiPart("Schema", "not found");
+
+        assertFalse("Should match first substring", shouldInclude("A specific 
error occurred"));
+        assertFalse("Should match second substring", shouldInclude("A warning 
message was issued"));
+        assertFalse("Should match multi-part", shouldInclude("Schema not 
found"));
+        assertTrue("Should not match any pattern", shouldInclude("Normal log 
message"));
+    }
+
+    @Test
+    public void testPrefixOptimization() {
+        // Add multiple substrings with same prefix to test prefix grouping
+        logChecker.addIgnoredSubstring("Schema not found");
+        logChecker.addIgnoredSubstring("Schema validation");
+        logChecker.addIgnoredSubstring("Schema error");
+
+        assertFalse("Should match first", shouldInclude("Schema not found for 
event"));
+        assertFalse("Should match second", shouldInclude("Schema validation 
failed"));
+        assertFalse("Should match third", shouldInclude("Schema error 
occurred"));
+        assertTrue("Should not match", shouldInclude("No schema issues"));
+    }
+
+    @Test
+    public void testShortSubstrings() {
+        logChecker.addIgnoredSubstring("err");
+        logChecker.addIgnoredSubstring("warn");
+
+        assertFalse("Should match short substring", shouldInclude("An error 
occurred"));
+        assertFalse("Should match short substring", shouldInclude("A warning 
was issued"));
+    }
+
+    @Test
+    public void testEmptySubstrings() {
+        // Should handle empty/null gracefully - they are filtered out and 
don't match
+        // Test with completely empty patterns only
+        LogChecker emptyChecker = LogChecker.builder().withErrorContext(0, 
0).withWarningContext(0, 0).build();
+        emptyChecker.addIgnoredSubstring("");
+        emptyChecker.addIgnoredSubstring(null);
+        emptyChecker.addIgnoredMultiPart();
+
+        LogChecker.LogEntry entry = emptyChecker.new LogEntry(
+            "10:00:00.000", "ERROR", "test-thread", "TestLogger", "Any 
message", 1L
+        );
+        assertTrue("Completely empty patterns should not match", 
emptyChecker.shouldIncludeEntry(entry));
+
+        // Test that filtering empty parts from multi-part works correctly
+        logChecker.addIgnoredMultiPart("", "test");
+        // Empty string is filtered, leaving just "test" as single-part
+        assertFalse("Filtered multi-part leaves 'test' which matches", 
shouldInclude("test message"));
+    }
+
+    @Test
+    public void testSubstringAtStart() {
+        logChecker.addIgnoredSubstring("Start");
+
+        assertFalse("Should match at start", shouldInclude("Start of 
message"));
+        assertFalse("Should match anywhere (substring matching)", 
shouldInclude("Message Start here"));
+    }
+
+    @Test
+    public void testSubstringAtEnd() {
+        logChecker.addIgnoredSubstring("End");
+
+        assertFalse("Should match at end", shouldInclude("Message ends with 
End"));
+        assertFalse("Should match anywhere (substring matching)", 
shouldInclude("End is in the middle"));
+    }
+
+    @Test
+    public void testSubstringInMiddle() {
+        logChecker.addIgnoredSubstring("middle");
+
+        assertFalse("Should match in middle", shouldInclude("Start middle 
end"));
+        assertFalse("Should match at start", shouldInclude("middle end"));
+        assertFalse("Should match at end", shouldInclude("Start middle"));
+    }
+
+    @Test
+    public void testOverlappingSubstrings() {
+        logChecker.addIgnoredSubstring("abc");
+        logChecker.addIgnoredSubstring("bcd");
+        logChecker.addIgnoredSubstring("cde");
+
+        assertFalse("Should match first", shouldInclude("abc found"));
+        assertFalse("Should match second", shouldInclude("bcd found"));
+        assertFalse("Should match third", shouldInclude("cde found"));
+        assertFalse("Should match overlapping", shouldInclude("abcde found"));
+    }
+
+    @Test
+    public void testVeryLongSubstring() {
+        StringBuilder longPattern = new StringBuilder(200);
+        for (int i = 0; i < 50; i++) {
+            longPattern.append("word").append(i).append(" ");
+        }
+        logChecker.addIgnoredSubstring(longPattern.toString().trim());
+
+        assertFalse("Should match long substring", shouldInclude("Prefix " + 
longPattern.toString().trim() + " suffix"));
+        assertTrue("Should not match partial", shouldInclude("word1 word2 
word3"));
+    }
+
+    @Test
+    public void testMultiPartWithOverlapping() {
+        logChecker.addIgnoredMultiPart("abc", "def", "ghi");
+
+        assertFalse("Should match all parts", shouldInclude("abc then def then 
ghi"));
+        assertFalse("Should match with text between", shouldInclude("abc def 
ghi"));
+        assertTrue("Should not match if parts missing (ghi missing)", 
shouldInclude("abc def"));
+        assertTrue("Should not match if order wrong", shouldInclude("def abc 
ghi"));
+        assertTrue("Should not match if only first part", shouldInclude("abc 
only"));
+    }
+
+    @Test
+    public void testMultiPartWithSamePart() {
+        // Use a more specific pattern to avoid matching "test" in "TestLogger"
+        logChecker.addIgnoredMultiPart("part", "part", "part");
+
+        assertFalse("Should match all three parts", shouldInclude("part part 
part"));
+        assertFalse("Should match all three parts with extra", 
shouldInclude("part part part extra"));
+
+        // "part part" should NOT match "part part part" pattern (missing 
third part)
+        LogChecker.LogEntry entry1 = logChecker.new LogEntry(
+            "10:00:00.000", "ERROR", "test-thread", "TestLogger", "part part", 
1L
+        );
+        assertTrue("Entry with only two 'part' should not match three-part 
pattern",
+            logChecker.shouldIncludeEntry(entry1));
+
+        assertTrue("Should not match if only one part", shouldInclude("part 
only"));
+    }
+
+    @Test
+    public void testMultiPartWithManyParts() {
+        logChecker.addIgnoredMultiPart("part1", "part2", "part3", "part4", 
"part5");
+
+        assertFalse("Should match all parts in sequence",
+            shouldInclude("part1 then part2 then part3 then part4 then 
part5"));
+        assertTrue("Should not match if not all parts present",
+            shouldInclude("part1 then part2 then part3"));
+    }
+
+    @Test
+    public void testCaseSensitivity() {
+        logChecker.addIgnoredSubstring("CaseSensitive");
+
+        assertFalse("Should match exact case", shouldInclude("CaseSensitive 
match"));
+        assertFalse("Should match lowercase", shouldInclude("casesensitive 
match"));
+        assertFalse("Should match uppercase", shouldInclude("CASESENSITIVE 
match"));
+        assertFalse("Should match mixed case", shouldInclude("CaSeSeNsItIvE 
match"));
+    }
+
+    @Test
+    public void testSpecialCharacters() {
+        logChecker.addIgnoredSubstring("[email protected]");
+        logChecker.addIgnoredSubstring("path/to/file");
+        logChecker.addIgnoredSubstring("value=123");
+
+        assertFalse("Should match email", shouldInclude("Contact 
[email protected] for help"));
+        assertFalse("Should match path", shouldInclude("File at path/to/file 
found"));
+        assertFalse("Should match equals", shouldInclude("Setting value=123"));
+    }
+
+    @Test
+    public void testUnicodeCharacters() {
+        logChecker.addIgnoredSubstring("café");
+        logChecker.addIgnoredSubstring("naïve");
+
+        assertFalse("Should match unicode", shouldInclude("Visit the café"));
+        assertFalse("Should match unicode", shouldInclude("A naïve approach"));
+    }
+
+    @Test
+    public void testWhitespaceHandling() {
+        logChecker.addIgnoredSubstring("test message");
+        logChecker.addIgnoredSubstring("  spaced  ");
+
+        assertFalse("Should match with single space", shouldInclude("This is a 
test message here"));
+        assertFalse("Should match with multiple spaces", shouldInclude("This 
has   spaced   in it"));
+    }
+
+    @Test
+    public void testNoSubstringsConfigured() {
+        // With no substrings, all entries should be included
+        assertTrue("Should include when no substrings configured", 
shouldInclude("Any message"));
+        assertTrue("Should include error messages", shouldInclude("ERROR 
occurred"));
+    }
+
+    @Test
+    public void testBundleWatcherFastPath() {
+        // BundleWatcher warnings are handled by fast path (no substring 
matching needed)
+        LogChecker.LogEntry warnEntry = logChecker.new LogEntry(
+            "10:00:00.000", "WARN", "test-thread",
+            "org.apache.unomi.lifecycle.BundleWatcher", "Some warning", 1L
+        );
+
+        assertFalse("BundleWatcher warnings should be ignored", 
logChecker.shouldIncludeEntry(warnEntry));
+    }
+
+    @Test
+    public void testCandidateStringIncludesLevelAndLogger() {
+        // Verify that matching works across level + logger + message
+        logChecker.addIgnoredSubstring("ERROR");
+
+        // ERROR appears in level, should match
+        assertFalse("Should match ERROR in level", shouldInclude("Some 
message"));
+
+        // Reset and test logger
+        logChecker = LogChecker.builder().withErrorContext(0, 
0).withWarningContext(0, 0).build();
+        logChecker.addIgnoredSubstring("TestLogger");
+
+        assertFalse("Should match logger name", shouldInclude("Some message"));
+    }
+
+    @Test
+    public void testPerformanceWithManySubstrings() {
+        // Add many substrings to test performance
+        for (int i = 0; i < 100; i++) {
+            logChecker.addIgnoredSubstring("pattern" + i);
+        }
+
+        // Should still match quickly
+        long start = System.nanoTime();
+        assertFalse("Should match pattern50", shouldInclude("This message 
contains pattern50 in it"));
+        long duration = System.nanoTime() - start;
+
+        // Should complete in reasonable time (< 1ms for this test)
+        assertTrue("Matching should be fast: " + duration + " ns", duration < 
1_000_000);
+    }
+
+    @Test
+    public void testPerformanceWithLongString() {
+        logChecker.addIgnoredSubstring("target");
+
+        // Create a long string (simulating a log entry with stack trace)
+        // Put target near the beginning to ensure it's within 
MAX_CANDIDATE_LENGTH
+        StringBuilder longString = new StringBuilder(10000);
+        longString.append("target "); // Put target at start
+        for (int i = 0; i < 1000; i++) {
+            longString.append("This is line ").append(i).append(" of a very 
long log message. ");
+        }
+
+        long start = System.nanoTime();
+        assertFalse("Should match target in long string", 
shouldInclude(longString.toString()));
+        long duration = System.nanoTime() - start;
+
+        // Should complete quickly even with long string (< 10ms)
+        assertTrue("Matching should be fast even with long strings: " + 
duration + " ns", duration < 10_000_000);
+    }
+
+    @Test
+    public void testPerformanceStressTest() {
+        // Comprehensive performance test with multiple patterns and long 
strings
+        // Should complete in under 2 seconds
+        long overallStart = System.currentTimeMillis();
+
+        // Add many diverse patterns
+        for (int i = 0; i < 50; i++) {
+            logChecker.addIgnoredSubstring("pattern" + i);
+            logChecker.addIgnoredSubstring("error" + i);
+            logChecker.addIgnoredMultiPart("part" + i, "sub" + i);
+        }
+
+        // Test many candidate strings
+        for (int i = 0; i < 1000; i++) {
+            String candidate = "Test message " + i + " with pattern" + (i % 
50) + " in it";
+            logChecker.shouldIncludeEntry(logChecker.new LogEntry(
+                "10:00:00.000", "ERROR", "test-thread", "TestLogger", 
candidate, 1L
+            ));
+        }
+
+        long overallDuration = System.currentTimeMillis() - overallStart;
+
+        // Should complete in under 2 seconds
+        assertTrue("Performance stress test should complete quickly: " + 
overallDuration + " ms",
+            overallDuration < 2000);
+    }
+
+    @Test
+    public void testTruncatedCandidateString() {
+        // Test that matching works even when candidate is truncated to 
MAX_CANDIDATE_LENGTH
+        logChecker.addIgnoredSubstring("early");
+
+        // Create a very long message that will be truncated
+        StringBuilder veryLongMessage = new StringBuilder(20000);
+        veryLongMessage.append("early "); // Put target at start
+        for (int i = 0; i < 2000; i++) {
+            veryLongMessage.append("This is a very long line 
").append(i).append(". ");
+        }
+
+        assertFalse("Should match even in truncated string", 
shouldInclude(veryLongMessage.toString()));
+    }
+
+    @Test
+    public void testPrefixLengthBoundary() {
+        // Test patterns at the PREFIX_LENGTH boundary (4 characters)
+        logChecker.addIgnoredSubstring("test"); // Exactly 4 chars
+        logChecker.addIgnoredSubstring("tes");  // 3 chars (short)
+        logChecker.addIgnoredSubstring("test1"); // 5 chars (prefix-based)
+
+        assertFalse("Should match 4-char pattern", shouldInclude("This is a 
test message"));
+        assertFalse("Should match 3-char pattern", shouldInclude("This has tes 
in it"));
+        assertFalse("Should match 5-char pattern", shouldInclude("This has 
test1 in it"));
+    }
+
+    /**
+     * Helper method to test if a message should be included (not ignored)
+     */
+    private boolean shouldInclude(String message) {
+        // Create a minimal log entry for testing
+        LogChecker.LogEntry entry = logChecker.new LogEntry(
+            "10:00:00.000", "ERROR", "test-thread",
+            "TestLogger", message, 1L
+        );
+
+        // shouldIncludeEntry is package-private, so we can call it directly
+        return logChecker.shouldIncludeEntry(entry);
+    }
+}

Reply via email to