elastic/elasticsearch · error · IllegalArgumentException

circular reference detected: {}

Error message

circular reference detected: {}

What it means

Thrown by PatternBank's constructor (and extendWith) when a named grok pattern transitively references itself. The constructor runs forbidCircularReferences, which depth-first walks the directed graph of %{NAME} references; when the walk returns to the start node with a non-trivial stack, the path is reported as a cycle. The message lists the offending reference chain (start->...->start) so the conflicting pattern definitions are visible.

Source

Thrown at libs/grok/src/main/java/org/elasticsearch/grok/PatternBank.java:109

             */
            Deque<String[]> stack = new ArrayDeque<>();
            stack.push(new String[] { traversalStartNode });
            // This is used so that we know that we're unwinding the stack and know not to get the current node's neighbors again.
            boolean unwinding = false;
            while (stack.isEmpty() == false) {
                String[] currentLevel = stack.peek();
                int firstNonNullIndex = findFirstNonNull(currentLevel);
                String node = currentLevel[firstNonNullIndex];
                boolean endOfThisPath = false;
                if (unwinding) {
                    // We have completed all of this node's neighbors and have popped back to the node
                    endOfThisPath = true;
                } else if (traversalStartNode.equals(node) && stack.size() > 1) {
                    Deque<String> reversedPath = new ArrayDeque<>();
                    for (String[] level : stack) {
                        reversedPath.push(level[findFirstNonNull(level)]);
                    }
                    throw new IllegalArgumentException("circular reference detected: " + String.join("->", reversedPath));
                } else if (visitedFromThisStartNode.contains(node)) {
                    /*
                     * We are only looking for a cycle starting and ending at traversalStartNode right now. But this node has been
                     * visited more than once in the path rooted at traversalStartNode. This could be because it is a cycle, or could be
                     * because two nodes in the path both point to it. We add it to nodesVisitedMoreThanOnceInAPath so that we make sure
                     * to check the path rooted at this node later.
                     */
                    nodesVisitedMoreThanOnceInAPath.add(node);
                    endOfThisPath = true;
                } else {
                    visitedFromThisStartNode.add(node);
                    String[] neighbors = getPatternNamesForPattern(bank, node);
                    if (neighbors.length == 0) {
                        endOfThisPath = true;
                    } else {
                        stack.push(neighbors);
                    }
                }

View on GitHub (pinned to db6a809a66)

Solutions

  1. Read the -> chain in the message: it names the exact patterns forming the loop (e.g. A->B->A). Break the cycle by removing or rewriting one of those entries.
  2. Audit every %{NAME} token in each named pattern in the chain and confirm each NAME resolves to a definition that does not lead back to the original.
  3. Build the bank incrementally with extendWith to bisect which added pattern introduced the cycle.
  4. If the cycle is intentional (aliasing), expand the alias inline so the graph stays acyclic, since PatternBank forbids cycles outright.

Example fix

// before
Map<String,String> patterns = new LinkedHashMap<>();
patterns.put("IP", "%{HOSTNAME}");
patterns.put("HOSTNAME", "%{IP}"); // cycle
PatternBank bank = new PatternBank(patterns); // throws

// after
patterns.put("IP", "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
patterns.put("HOSTNAME", "\\b(?:[a-zA-Z0-9]+\\.)+[a-zA-Z]+\\b");
PatternBank bank = new PatternBank(patterns); // ok
Defensive patterns

Strategy: validation

Validate before calling

// Validate a pattern map for cycles before constructing the bank.
static void assertAcyclic(java.util.Map<String,String> patterns) {
    java.util.Set<String> visiting = new java.util.HashSet<>();
    java.util.Set<String> done = new java.util.HashSet<>();
    for (String name : patterns.keySet()) dfs(name, patterns, visiting, done);
}
private static void dfs(String name, java.util.Map<String,String> patterns,
                        java.util.Set<String> visiting, java.util.Set<String> done) {
    if (done.contains(name) || !patterns.containsKey(name)) return;
    if (!visiting.add(name)) throw new IllegalArgumentException("cycle at " + name);
    String body = patterns.get(name);
    java.util.regex.Matcher m = java.util.regex.Pattern.compile("%\\{(\\w+)").matcher(body);
    while (m.find()) dfs(m.group(1), patterns, visiting, done);
    visiting.remove(name);
    done.add(name);
}

Type guard

// Narrow to a bank whose construction succeeded (no cycle possible post-construction)
static boolean isAcyclicMap(java.util.Map<String,String> patterns) {
    try { new org.elasticsearch.grok.PatternBank(patterns); return true; }
    catch (IllegalArgumentException e) { return false; }
}

Try / catch

try {
    PatternBank bank = new PatternBank(userPatterns);
} catch (IllegalArgumentException e) {
    if (e.getMessage().startsWith("circular reference detected")) {
        // e.getMessage() contains the offending A->B->A chain
        reportConfigError("grok patterns contain a cycle: " + e.getMessage());
    } else throw e;
}

Prevention

When it happens

Trigger: Constructing `new PatternBank(map)` or calling `patternBank.extendWith(extra)` where two or more entries form a cycle, e.g. {"A":"%{B}", "B":"%{A}"} or a self-reference {"A":"%{A}"}. The check runs once at construction time, so the exception surfaces at the new PatternBank(...) call, not at match time.

Common situations: Loading a user-supplied grok pattern catalog (Logstash-style patterns.conf, ingest grok processor patterns) where one definition was renamed but a stale reference remained; copy-pasting a pattern that delegates to another which was later edited to delegate back; merging two pattern sets that each redefine a name pointing at the other.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/40d35b0361a827ae. Report an issue: GitHub.