Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 44 additions & 33 deletions bugzooka/analysis/jsonparser.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,61 @@
import re
import json
import logging

logger = logging.getLogger(__name__)

SEPARATOR = "\u2500" * 60

def extract_json_changepoints(json_data):

def extract_json_changepoints(json_data, max_prs=None):
"""
Extract changepoints from JSON changepoint summaries.
Extract changepoint summaries from JSON data.

Each changepoint entry produces a multi-line block with version info,
regressed metrics, and the PRs introduced between nightlies.

:param json_data: List of changepoint records
:return: list of changepoint strings
:param max_prs: Maximum PRs to display per changepoint (None = all)
:return: list of formatted changepoint summary strings (one per entry)
"""
changepoints = []
for entry in json_data:
if not entry.get("is_changepoint", False):
continue
cp_entries = [e for e in json_data if e.get("is_changepoint", False)]
total = len(cp_entries)

build_url = entry.get("buildUrl", "N/A")
changepoints = []
for idx, entry in enumerate(cp_entries, 1):
github_ctx = entry.get("github_context", {})
current_version = github_ctx.get(
"current_version", entry.get("ocpVersion", "unknown")
)
previous_version = github_ctx.get("previous_version", "unknown")
prs = entry.get("prs", [])
metrics = entry.get("metrics", {})

regressed = []
for metric_name, metric_data in metrics.items():
percentage = metric_data.get("percentage_change", 0)
if percentage != 0: # only flag actual changepoints
label_string = metric_data.get("labels", "")
url = re.sub(r"X+-X+", "ocp-qe-perfscale", build_url.strip(), count=1)
changepoints.append(
f"{label_string} {metric_name} regression detection --- {percentage} % changepoint --- {url}"
)

return changepoints
if percentage != 0:
sign = "+" if percentage > 0 else ""
regressed.append(f"{metric_name}: {sign}{percentage:.2f}%")

if not regressed:
continue

def summarize_orion_json(json_path):
"""
Summarize a given json file.
regressed_summary = ", ".join(regressed)
lines = [
f"{SEPARATOR}",
f" Changepoint {idx} of {total}: {regressed_summary}",
f"{SEPARATOR}",
f"Version: {current_version}",
f"Previous: {previous_version}",
]

if prs:
display_prs = prs[:max_prs] if max_prs is not None else prs
lines.append(f"\nPRs between nightlies ({len(prs)}):")
for pr in display_prs:
lines.append(f" {pr}")
if max_prs is not None and len(prs) > max_prs:
lines.append(f" ... and {len(prs) - max_prs} more")

changepoints.append("\n".join(lines))

:param json_path: json file path
:return: summary of the json file
"""
with open(json_path, "r") as f:
json_data = json.load(f)
summaries = []
changepoints = extract_json_changepoints(json_data)
for entry in json_data:
if entry.get("is_changepoint", False):
for cp in changepoints:
summaries.append(f"\n--- Test Case: {cp} ---")
return "".join(summaries)
return changepoints
11 changes: 9 additions & 2 deletions bugzooka/analysis/log_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from bugzooka.integrations import mcp_client as mcp_module
from bugzooka.integrations.mcp_client import initialize_global_resources_async
from bugzooka.core.config import get_prompt_config
from bugzooka.analysis.prow_analyzer import analyze_prow_artifacts
from bugzooka.analysis.prow_analyzer import analyze_prow_artifacts, ProwAnalysisResult
from bugzooka.core.utils import extract_job_details

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -129,7 +129,14 @@ def download_and_analyze_logs(text):
"""Extract job details, download and analyze logs."""
job_url, job_name = extract_job_details(text)
if job_url is None or job_name is None:
return None, None, None, None
return ProwAnalysisResult(
errors=None,
categorization_message=None,
requires_llm=None,
is_install_issue=None,
step_name=None,
full_errors_for_file=None,
)
directory_path = download_prow_logs(job_url)
return analyze_prow_artifacts(directory_path, job_name)

Expand Down
46 changes: 46 additions & 0 deletions bugzooka/analysis/log_summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,52 @@ def download_prow_logs(url, output_dir="/tmp/"):
return log_dir


def construct_visualization_url(view_url, step_name):
"""
Build a gcsweb URL pointing to the step's artifacts directory.
:param view_url: prow view URL
:param step_name: raw step name from junit_operator.xml
:return: gcsweb URL string, or None if the log folder cannot be resolved
"""
try:
gcs_path = view_url.split("view/gs/")[1]
base = "https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/"
artifact_root = f"gs://{gcs_path}/artifacts/"
top_folders = list_gcs_files(artifact_root)

# Find the folder that actually contains the step as a subfolder.
# The junit step_name often includes the log_folder as a prefix
# (e.g. "payload-control-plane-6nodes-openshift-qe-orion-udn-density")
# while the GCS folder is just "openshift-qe-orion-udn-density".
for entry in top_folders:
if not entry.rstrip().endswith("/"):
continue
folder = entry.strip("/").split("/")[-1]
# Try with prefix stripped first, then the raw step_name
candidates = [step_name]
prefix = folder + "-"
if step_name.startswith(prefix):
candidates.insert(0, step_name[len(prefix) :])
for candidate in candidates:
step_artifacts = f"{artifact_root}{folder}/{candidate}/artifacts/"
try:
files = list_gcs_files(step_artifacts)
except Exception:
continue
artifacts_path = f"{gcs_path}/artifacts/{folder}/{candidate}/artifacts/"
html_files = [f for f in files if f.endswith(".html")]
if html_files:
html_name = html_files[0].strip("/").split("/")[-1]
return f"{base}{artifacts_path}{html_name}"
return f"{base}{artifacts_path}"

return None
except Exception as e:
logger.error("Failed to construct visualization URL: %s", e)
return None


def get_logjuicer_extract(directory_path, job_name):
"""Extracts erros using logjuicer using fallback mechanism.
Expand Down
Loading