Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
136d54d
sync with errorscraper
sunnyhe2 Jul 6, 2026
d0b3cf6
Merge branch 'development' into sunny_rdmaethool_enhance
sunnyhe2 Jul 6, 2026
b6c59de
fix exclusion regex
sunnyhe2 Jul 6, 2026
f44d70d
switch error field add
sunnyhe2 Jul 6, 2026
e9c1c9f
removed empty collector arg
sunnyhe2 Jul 7, 2026
dc4079f
Merge branch 'development' into sunny_switch_enhancement
sunnyhe2 Jul 7, 2026
e9d6003
Merge pull request #248 from amd/sunny_switch_enhancement
sunnyhe2 Jul 7, 2026
88e42f1
Merge branch 'development' into sunny_rdmaethool_enhance
sunnyhe2 Jul 7, 2026
1fd9641
Merge pull request #246 from amd/sunny_rdmaethool_enhance
sunnyhe2 Jul 7, 2026
d7cfa61
docs: Update plugin documentation [automated]
github-actions[bot] Jul 7, 2026
abe4dc5
ignorning base class from docs
alexandraBara Jul 7, 2026
a093fd3
Merge pull request #251 from amd/automated-plugin-docs-update
alexandraBara Jul 7, 2026
61be38f
Merge branch 'development' into alex_utest_speedup
alexandraBara Jul 7, 2026
3b23def
Merge pull request #252 from amd/alex_utest_speedup
alexandraBara Jul 7, 2026
a6dfd68
docs: Update plugin documentation [automated]
github-actions[bot] Jul 8, 2026
385b7a4
updates
alexandraBara Jul 8, 2026
ccf8d2b
fix for mce block jitter
alexandraBara Jul 8, 2026
dac0311
Merge pull request #253 from amd/automated-plugin-docs-update
alexandraBara Jul 8, 2026
1f7c396
Merge branch 'development' into alex_mce_update
alexandraBara Jul 8, 2026
1425c10
Merge pull request #254 from amd/alex_mce_update
alexandraBara Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 18 additions & 30 deletions docs/PLUGIN_DOC.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/generate_plugin_doc_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
# ``nodescraper.plugins`` itself does not match every module starting with that string.
PLUGIN_MODULE_PREFIX = f"{PACKAGE_PLUGINS_ROOT}."
DEFAULT_PACKAGES = (PACKAGE_PLUGINS_ROOT,)
# OOB plugin stubs with no concrete COLLECTOR/ANALYZER; document subclasses only.
PLUGIN_DOC_IGNORE_CLASSES = frozenset({"ServiceabilityPluginBase"})


def get_attr(obj: Any, name: str, default: Any = None) -> Any:
Expand Down Expand Up @@ -230,6 +232,8 @@ def plugins_for_package_prefix(base_classes: Iterable[type], package_prefix: str
continue
if not is_concrete_plugin_class(cls):
continue
if cls.__name__ in PLUGIN_DOC_IGNORE_CLASSES:
continue
found.append(cls)
return found

Expand Down
47 changes: 46 additions & 1 deletion nodescraper/base/regexanalyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,34 @@ def _line_at_match_position(self, content: str, match_start: int) -> str:
line_end = len(content)
return content[line_start:line_end]

def _line_index_at_position(self, content: str, position: int) -> int:
if position <= 0:
return 0
return content.count("\n", 0, position)

def _match_intersects_skip_lines(
self,
content: str,
match_start: int,
match_end: int,
skip_line_indices: frozenset[int],
) -> bool:
if not skip_line_indices:
return False
start_line = self._line_index_at_position(content, match_start)
end_line = self._line_index_at_position(content, max(match_start, match_end - 1))
return any(line_idx in skip_line_indices for line_idx in range(start_line, end_line + 1))

def _match_on_skipped_line(
self,
content: str,
match_start: int,
skip_line_indices: frozenset[int],
) -> bool:
if not skip_line_indices:
return False
return self._line_index_at_position(content, match_start) in skip_line_indices

def _should_ignore_regex_match(
self,
content: str,
Expand Down Expand Up @@ -245,6 +273,7 @@ def check_all_regexes(
num_timestamps: int = 3,
interval_to_collapse_event: int = 60,
ignore_match_rules: Optional[Sequence[ParsedIgnoreMatchRule]] = None,
skip_line_indices: Optional[frozenset[int]] = None,
) -> list[RegexEvent]:
"""Iterate over all ERROR_REGEX and check content for any matches

Expand All @@ -262,6 +291,7 @@ def check_all_regexes(
num_timestamps (int, optional): maximum number of timestamps to keep for each event. Defaults to 3.
interval_to_collapse_event (int, optional): time interval in seconds to collapse events. Defaults to 60.
ignore_match_rules (Optional[Sequence[ParsedIgnoreMatchRule]], optional): Parsed skip rules. Defaults to None.
skip_line_indices (Optional[frozenset[int]], optional): Line indices to skip entirely. Defaults to None.

Returns:
list[RegexEvent]: list of regex event objects
Expand Down Expand Up @@ -296,17 +326,30 @@ def _is_within_interval(new_timestamp_str: str, existing_timestamps: list[str])
return False

skip_rules = list(ignore_match_rules) if ignore_match_rules else []
suppressed_lines = skip_line_indices or frozenset()

for error_regex_obj in error_regex:
for match_obj in error_regex_obj.regex.finditer(content):
search_from = 0
while search_from <= len(content):
match_obj = error_regex_obj.regex.search(content, search_from)
if match_obj is None:
break
raw_match = match_obj.group(0)
if self._match_on_skipped_line(
content,
match_obj.start(),
suppressed_lines,
):
search_from = match_obj.start() + 1
continue
if self._should_ignore_regex_match(
content,
match_obj.start(),
raw_match,
error_regex_obj.message,
skip_rules,
):
search_from = match_obj.start() + 1
continue

# Extract timestamp from the line where match occurs
Expand Down Expand Up @@ -360,6 +403,8 @@ def _is_within_interval(new_timestamp_str: str, existing_timestamps: list[str])

regex_event_list.append(new_event)

search_from = match_obj.end()

all_events = list(regex_map.values()) if group else regex_event_list

# Prune timestamp lists to keep only first N and last N timestamps
Expand Down
59 changes: 59 additions & 0 deletions nodescraper/command_artifact_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,17 @@
}
pre.stderr { color: var(--fail-fg); }
.no-results { color: var(--muted); padding: 24px; text-align: center; display: none; }
.copy-btn {
flex: 0 0 auto;
display: inline-flex; align-items: center; justify-content: center;
background: var(--bg); color: var(--muted);
border: 1px solid var(--border); border-radius: 6px;
padding: 5px 7px; cursor: pointer; line-height: 0;
transition: color .15s ease, border-color .15s ease, background .15s ease;
}
.copy-btn:hover { background: var(--panel-hover); border-color: var(--accent); color: var(--text); }
.copy-btn.copied { color: var(--ok-fg); border-color: var(--ok-fg); }
.copy-btn svg { display: block; }
</style>
</head>
<body>
Expand All @@ -143,6 +154,9 @@
<span class="chevron">&#9656;</span>
<code class="title">{command}</code>
<span class="badge {badge_cls}">exit {exit_code}</span>
<button class="copy-btn" type="button" title="Copy output" aria-label="Copy output">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
</button>
</summary>
<div class="body">
{stdout_block}
Expand Down Expand Up @@ -176,6 +190,51 @@
document.getElementById('collapseAll').addEventListener('click', () => {
items.forEach(d => d.open = false);
});

const copyButtons = Array.from(document.querySelectorAll('.copy-btn'));
const COPY_SVG = copyButtons.length ? copyButtons[0].innerHTML : '';
const CHECK_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>';

function showCopied(btn) {
btn.classList.add('copied');
btn.innerHTML = CHECK_SVG;
if (btn._resetTimer) clearTimeout(btn._resetTimer);
btn._resetTimer = setTimeout(() => {
btn.classList.remove('copied');
btn.innerHTML = COPY_SVG;
}, 1200);
}

function fallbackCopy(text, btn) {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.top = '-1000px';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.focus();
ta.select();
try { document.execCommand('copy'); showCopied(btn); } catch (e) {}
document.body.removeChild(ta);
}

copyButtons.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const card = btn.closest('details.cmd');
const parts = [];
card.querySelectorAll('pre').forEach(pre => {
if (!pre.classList.contains('empty')) parts.push(pre.textContent);
});
const text = parts.join('\\n');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => showCopied(btn)).catch(() => fallbackCopy(text, btn));
} else {
fallbackCopy(text, btn);
}
});
});
</script>
</body>
</html>
Expand Down
4 changes: 3 additions & 1 deletion nodescraper/plugins/inband/dmesg/analyzer_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class DmesgAnalyzerArgs(TimeRangeAnalysisArgs):
"Rules that skip regex matches during analysis. Each rule may use line_regex, "
"match_regex, message, and/or mce_banks. Within a rule all specified fields must "
"match; any matching rule suppresses the hit. mce_banks accepts bank ids and "
'inclusive ranges such as "60-63".'
'inclusive ranges such as "60-63". When a rule matches an MCA bank, only '
"[Hardware Error]: lines in that incident block are suppressed; interleaved "
"non-MCE dmesg lines in the same block are not."
),
)
11 changes: 10 additions & 1 deletion nodescraper/plugins/inband/dmesg/dmesg_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@

from .analyzer_args import DmesgAnalyzerArgs
from .dmesgdata import DmesgData
from .mce_utils import parse_correctable_mce_counts, parse_uncorrectable_mce_counts
from .mce_utils import (
ignored_mce_block_line_indices,
mce_block_all_line_indices,
parse_correctable_mce_counts,
parse_uncorrectable_mce_counts,
)


class DmesgAnalyzer(RegexAnalyzer[DmesgData, DmesgAnalyzerArgs]):
Expand Down Expand Up @@ -714,6 +719,8 @@ def analyze_data(
dmesg_content = data.dmesg_content

ignore_match_rules, ignore_mce_banks = parse_ignore_match_rules(args.ignore_match_rules)
ignored_mce_block_lines = ignored_mce_block_line_indices(dmesg_content, ignore_mce_banks)
mce_block_lines = mce_block_all_line_indices(dmesg_content)

known_err_events = self.check_all_regexes(
content=dmesg_content,
Expand All @@ -722,6 +729,7 @@ def analyze_data(
num_timestamps=args.num_timestamps,
interval_to_collapse_event=args.interval_to_collapse_event,
ignore_match_rules=ignore_match_rules,
skip_line_indices=ignored_mce_block_lines,
)
if args.exclude_category:
known_err_events = [
Expand Down Expand Up @@ -752,6 +760,7 @@ def analyze_data(
num_timestamps=args.num_timestamps,
interval_to_collapse_event=args.interval_to_collapse_event,
ignore_match_rules=ignore_match_rules,
skip_line_indices=mce_block_lines,
)

for err_event in err_events:
Expand Down
Loading