Skip to content

Commit 25c8041

Browse files
committed
Add regex pattern support for error detection
This commit extends the error detection system with regex pattern matching, enabling complex error patterns with variable parts (URLs, IPs, hostnames). Changes: - Add regex_matches section to errors.yaml - DNS lookup errors: 'dial tcp:.*no such host' - Connection refused errors - Timeout errors (context deadline, i/o timeout, net/http timeout) - Handles user's example: Get "https://fred.brew.sh/...": dial tcp: lookup fred.brew.sh on 8.8.8.8:53: no such host - Update ErrorDetector class - Add regex_patterns list to store compiled regex objects - Compile patterns with re.IGNORECASE flag for case-insensitive matching - Check messages against regex patterns in is_error() method - Update extract_error_info() to return pattern_type ("fuzzy", "exact", or "regex") - Extend test suite with regex pattern tests - Test regex pattern loading and compilation - Test DNS lookup error detection (user's example) - Test connection refused errors - Test timeout errors - Test case-insensitive regex matching - Test error info extraction with pattern_type Now supports three pattern types: - Fuzzy: Fast substring matching for simple patterns - Exact: Precise prefix/exact matching - Regex: Flexible pattern matching for complex errors with variable parts Tested with user's DNS error example - successfully detected!
1 parent dba09f5 commit 25c8041

File tree

3 files changed

+116
-2
lines changed

3 files changed

+116
-2
lines changed

pystackql/core/error_detector.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import os
11+
import re
1112
import yaml
1213

1314

@@ -22,6 +23,7 @@ def __init__(self):
2223
"""Initialize the ErrorDetector by loading error patterns from errors.yaml."""
2324
self.fuzzy_patterns = []
2425
self.exact_patterns = []
26+
self.regex_patterns = [] # List of compiled regex pattern objects
2527
self._load_error_patterns()
2628

2729
def _load_error_patterns(self):
@@ -57,6 +59,18 @@ def _load_error_patterns(self):
5759
for pattern in errors['exact_matches']
5860
if pattern
5961
]
62+
63+
# Load regex patterns (compile them for efficiency)
64+
if 'regex_matches' in errors:
65+
self.regex_patterns = []
66+
for pattern in errors['regex_matches']:
67+
if pattern:
68+
try:
69+
# Compile with IGNORECASE flag for case-insensitive matching
70+
compiled = re.compile(pattern, re.IGNORECASE)
71+
self.regex_patterns.append((pattern, compiled))
72+
except re.error as regex_err:
73+
print(f"Warning: Invalid regex pattern '{pattern}': {regex_err}")
6074
except Exception as e:
6175
# If we can't load the error patterns, continue with empty lists
6276
# This ensures the module doesn't break existing functionality
@@ -86,6 +100,11 @@ def is_error(self, message):
86100
if message == pattern or message.startswith(pattern):
87101
return True
88102

103+
# Check regex matches
104+
for pattern_str, compiled_pattern in self.regex_patterns:
105+
if compiled_pattern.search(message):
106+
return True
107+
89108
return False
90109

91110
def extract_error_info(self, message):
@@ -102,20 +121,31 @@ def extract_error_info(self, message):
102121

103122
message_lower = message.lower()
104123
detected_pattern = None
124+
pattern_type = None
105125

106-
# Find which pattern was matched
126+
# Find which pattern was matched (check in order: fuzzy, exact, regex)
107127
for pattern in self.fuzzy_patterns:
108128
if pattern in message_lower:
109129
detected_pattern = pattern
130+
pattern_type = "fuzzy"
110131
break
111132

112133
if not detected_pattern:
113134
for pattern in self.exact_patterns:
114135
if message == pattern or message.startswith(pattern):
115136
detected_pattern = pattern
137+
pattern_type = "exact"
138+
break
139+
140+
if not detected_pattern:
141+
for pattern_str, compiled_pattern in self.regex_patterns:
142+
if compiled_pattern.search(message):
143+
detected_pattern = pattern_str
144+
pattern_type = "regex"
116145
break
117146

118147
return {
119148
"error": message,
120-
"detected_pattern": detected_pattern
149+
"detected_pattern": detected_pattern,
150+
"pattern_type": pattern_type
121151
}

pystackql/errors.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# Pattern Types:
88
# - fuzzy_matches: Substring matching (case-insensitive)
99
# - exact_matches: Exact string matching (case-sensitive)
10+
# - regex_matches: Regular expression matching (for complex patterns with variable parts)
1011

1112
errors:
1213
# Fuzzy matches - will match if the pattern appears anywhere in the message
@@ -30,3 +31,22 @@ errors:
3031
- "Error:"
3132
- "FAILED"
3233
- "FAILURE"
34+
35+
# Regex matches - regular expressions for complex error patterns
36+
# Use standard Python regex syntax (case-insensitive by default)
37+
regex_matches:
38+
# Network/DNS errors
39+
- 'dial tcp:.*no such host'
40+
- 'Get ".*".*dial tcp.*lookup.*no such host'
41+
42+
# Connection errors
43+
- 'dial tcp.*connection refused'
44+
- 'unable to connect to.*connection refused'
45+
46+
# Timeout errors
47+
- 'context deadline exceeded'
48+
- 'timeout.*waiting for'
49+
50+
# Generic network errors
51+
- 'dial tcp.*i/o timeout'
52+
- 'net/http.*timeout'

tests/test_error_detection.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,70 @@ def test_non_string_handling(self):
125125
assert not self.detector.is_error([])
126126
assert not self.detector.is_error({})
127127

128+
def test_regex_pattern_loading(self):
129+
"""Test that regex patterns are loaded and compiled."""
130+
assert len(self.detector.regex_patterns) > 0
131+
# Check that patterns are tuples of (pattern_str, compiled_regex)
132+
for item in self.detector.regex_patterns:
133+
assert isinstance(item, tuple)
134+
assert len(item) == 2
135+
pattern_str, compiled = item
136+
assert isinstance(pattern_str, str)
137+
# Check it's a compiled regex
138+
assert hasattr(compiled, 'search')
139+
140+
def test_regex_dns_error_detection(self):
141+
"""Test detection of DNS lookup errors using regex."""
142+
messages = [
143+
'Get "https://fred.brew.sh/api/formula/stackql.json?": dial tcp: lookup fred.brew.sh on 8.8.8.8:53: no such host',
144+
'dial tcp: lookup example.com on 1.1.1.1:53: no such host',
145+
'Get "http://api.example.com": dial tcp: lookup api.example.com on 192.168.1.1:53: no such host',
146+
]
147+
for msg in messages:
148+
assert self.detector.is_error(msg), f"Should detect DNS error in: {msg}"
149+
150+
def test_regex_connection_refused(self):
151+
"""Test detection of connection refused errors using regex."""
152+
messages = [
153+
'dial tcp 192.168.1.1:5432: connection refused',
154+
'dial tcp [::1]:8080: connection refused',
155+
'unable to connect to server: connection refused',
156+
]
157+
for msg in messages:
158+
assert self.detector.is_error(msg), f"Should detect connection error in: {msg}"
159+
160+
def test_regex_timeout_errors(self):
161+
"""Test detection of timeout errors using regex."""
162+
messages = [
163+
'context deadline exceeded',
164+
'dial tcp 10.0.0.1:443: i/o timeout',
165+
'net/http: request canceled while waiting for connection (Client.Timeout exceeded)',
166+
'timeout while waiting for response',
167+
]
168+
for msg in messages:
169+
assert self.detector.is_error(msg), f"Should detect timeout error in: {msg}"
170+
171+
def test_regex_case_insensitive(self):
172+
"""Test that regex matching is case-insensitive."""
173+
messages = [
174+
'DIAL TCP: NO SUCH HOST',
175+
'Context Deadline Exceeded',
176+
'Connection Refused',
177+
]
178+
for msg in messages:
179+
assert self.detector.is_error(msg), f"Should detect error (case-insensitive) in: {msg}"
180+
181+
def test_extract_error_info_with_regex(self):
182+
"""Test error info extraction for regex matches."""
183+
msg = 'Get "https://example.com": dial tcp: lookup example.com on 8.8.8.8:53: no such host'
184+
info = self.detector.extract_error_info(msg)
185+
assert info is not None
186+
assert info["error"] == msg
187+
assert info["pattern_type"] == "regex"
188+
assert info["detected_pattern"] is not None
189+
# Should match one of the DNS error patterns
190+
assert "no such host" in info["detected_pattern"]
191+
128192

129193
class TestOutputFormatterErrorDetection:
130194
"""Tests for error detection integration in OutputFormatter."""

0 commit comments

Comments
 (0)