monitoring(testing): split quality trend panels

This commit is contained in:
jenkins 2026-04-22 12:42:33 -03:00
parent 5d560d962d
commit 23beb08e5e
6 changed files with 2571 additions and 4048 deletions

View File

@ -3088,6 +3088,10 @@ def build_jobs_dashboard():
smell_selector = f'suite=~"{suite_var}",{exported}' smell_selector = f'suite=~"{suite_var}",{exported}'
test_case_selector = f'suite=~"{suite_var}",branch=~"{branch_var}",test=~"{test_var}",test!="__no_test_cases__",{exported}' test_case_selector = f'suite=~"{suite_var}",branch=~"{branch_var}",test=~"{test_var}",test!="__no_test_cases__",{exported}'
build_info_selector = f'suite=~"{suite_var}",branch=~"{branch_var}",{exported}' build_info_selector = f'suite=~"{suite_var}",branch=~"{branch_var}",{exported}'
selected_suite_universe = (
f'(sum by (suite) (increase(platform_quality_gate_runs_total{{{runs_selector}}}[30d])) >= bool 0)'
)
selected_suite_zero = f"(0 * {selected_suite_universe})"
suite_universe = " or ".join( suite_universe = " or ".join(
f'label_replace(vector(1), "suite", "{suite}", "__name__", ".*")' f'label_replace(vector(1), "suite", "{suite}", "__name__", ".*")'
@ -3169,7 +3173,7 @@ def build_jobs_dashboard():
core = ( core = (
f'sum by (suite) (max_over_time(({{{checks_selector},check=~"{regex}",{state}}})[$__interval]))' f'sum by (suite) (max_over_time(({{{checks_selector},check=~"{regex}",{state}}})[$__interval]))'
) )
return f'({core}) or on(suite) (0 * ({suite_universe}))' return f'({core}) or on(suite) ({selected_suite_zero})'
problematic_tests_history_core = ( problematic_tests_history_core = (
f'topk(12, sum by (suite, test, jenkins_job) (increase(platform_quality_gate_test_case_result{{suite=~"{suite_var}",branch=~"{branch_var}",test!="__no_test_cases__",status="failed",{exported}}}[$__interval])))' f'topk(12, sum by (suite, test, jenkins_job) (increase(platform_quality_gate_test_case_result{{suite=~"{suite_var}",branch=~"{branch_var}",test!="__no_test_cases__",status="failed",{exported}}}[$__interval])))'
@ -3196,6 +3200,10 @@ def build_jobs_dashboard():
"legendFormat": "skipped · {{suite}} · #{{build_number}}", "legendFormat": "skipped · {{suite}} · #{{build_number}}",
}, },
] ]
selected_test_pass_rate = (
f'100 * (sum by (suite, test) (max_over_time(platform_quality_gate_test_case_result{{{test_case_selector},status="passed"}}[$__interval]))) '
f'/ clamp_min((sum by (suite, test) (max_over_time(platform_quality_gate_test_case_result{{{test_case_selector},status=~"passed|failed|error|skipped"}}[$__interval]))), 1)'
)
recent_branch_evidence = ( recent_branch_evidence = (
f'sort_desc(count by (suite, branch) (max_over_time(platform_quality_gate_build_info{{{build_info_selector}}}[30d])))' f'sort_desc(count by (suite, branch) (max_over_time(platform_quality_gate_build_info{{{build_info_selector}}}[30d])))'
) )
@ -3222,16 +3230,21 @@ def build_jobs_dashboard():
missing_test_case_by_suite = _missing_suite_series( missing_test_case_by_suite = _missing_suite_series(
f"count by (suite) (platform_quality_gate_test_case_result{{{exported}}})" f"count by (suite) (platform_quality_gate_test_case_result{{{exported}}})"
) )
placeholder_test_case_by_suite = _missing_suite_series(
f'count by (suite) (platform_quality_gate_test_case_result{{{exported},test!="__no_test_cases__"}})'
)
success_thresholds = { success_thresholds = {
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
{"color": "red", "value": None}, {"color": "red", "value": None},
{"color": "orange", "value": 80}, {"color": "orange", "value": 90},
{"color": "yellow", "value": 95}, {"color": "yellow", "value": 93},
{"color": "green", "value": 99}, {"color": "green", "value": 95},
{"color": "blue", "value": 100},
], ],
} }
coverage_thresholds = success_thresholds
failures_thresholds = { failures_thresholds = {
"mode": "absolute", "mode": "absolute",
"steps": [ "steps": [
@ -3341,101 +3354,12 @@ def build_jobs_dashboard():
) )
) )
panels.append(
stat_panel(
19,
"Failing Tests",
checks_failed_tests,
{"h": 4, "w": 3, "x": 0, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append(
stat_panel(
20,
"Failing Coverage",
checks_failed_coverage,
{"h": 4, "w": 3, "x": 3, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append(
stat_panel(
21,
"Failing LOC",
checks_failed_loc,
{"h": 4, "w": 3, "x": 6, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append(
stat_panel(
22,
"Failing Docs/Naming",
checks_failed_docs,
{"h": 4, "w": 3, "x": 9, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append(
stat_panel(
23,
"Failing Gate/Glue",
checks_failed_gate,
{"h": 4, "w": 3, "x": 12, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append(
stat_panel(
24,
"Failing SonarQube",
checks_failed_sonarqube,
{"h": 4, "w": 3, "x": 15, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append(
stat_panel(
25,
"Failing Supply Chain",
checks_failed_supply_chain,
{"h": 4, "w": 3, "x": 18, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append(
stat_panel(
26,
"Total Failing Checks",
checks_failed_total,
{"h": 4, "w": 3, "x": 21, "y": 5},
unit="none",
instant=True,
thresholds=failures_thresholds,
)
)
panels.append( panels.append(
bargauge_panel( bargauge_panel(
8, 8,
"Failures by Suite (24h)", "Failures by Suite (24h)",
failures_by_suite_24h, failures_by_suite_24h,
{"h": 8, "w": 8, "x": 0, "y": 9}, {"h": 8, "w": 8, "x": 0, "y": 5},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3447,7 +3371,7 @@ def build_jobs_dashboard():
9, 9,
"Success Rate by Suite (24h)", "Success Rate by Suite (24h)",
success_rate_by_suite_24h, success_rate_by_suite_24h,
{"h": 8, "w": 8, "x": 8, "y": 9}, {"h": 8, "w": 8, "x": 8, "y": 5},
unit="percent", unit="percent",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3460,7 +3384,7 @@ def build_jobs_dashboard():
10, 10,
"Coverage Gap to 95% by Suite", "Coverage Gap to 95% by Suite",
coverage_gap, coverage_gap,
{"h": 8, "w": 8, "x": 16, "y": 9}, {"h": 8, "w": 8, "x": 16, "y": 5},
unit="percent", unit="percent",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3475,7 +3399,7 @@ def build_jobs_dashboard():
11, 11,
"Success History by Suite", "Success History by Suite",
success_history_by_suite, success_history_by_suite,
{"h": 8, "w": 24, "x": 0, "y": 17}, {"h": 8, "w": 24, "x": 0, "y": 13},
unit="percent", unit="percent",
legend="{{suite}}", legend="{{suite}}",
legend_display="list", legend_display="list",
@ -3499,7 +3423,7 @@ def build_jobs_dashboard():
12, 12,
"Run Outcomes (Selected Scope)", "Run Outcomes (Selected Scope)",
None, None,
{"h": 8, "w": 8, "x": 0, "y": 25}, {"h": 8, "w": 8, "x": 0, "y": 21},
unit="none", unit="none",
targets=[ targets=[
{ {
@ -3528,7 +3452,7 @@ def build_jobs_dashboard():
13, 13,
"Coverage & LOC History (Selected Scope)", "Coverage & LOC History (Selected Scope)",
None, None,
{"h": 8, "w": 8, "x": 8, "y": 25}, {"h": 8, "w": 8, "x": 8, "y": 21},
unit="none", unit="none",
targets=[ targets=[
{ {
@ -3551,213 +3475,66 @@ def build_jobs_dashboard():
14, 14,
"Run Status Mix (30d)", "Run Status Mix (30d)",
f'sum by (status) (increase(platform_quality_gate_runs_total{{{runs_selector}}}[30d]))', f'sum by (status) (increase(platform_quality_gate_runs_total{{{runs_selector}}}[30d]))',
{"h": 8, "w": 8, "x": 16, "y": 25}, {"h": 8, "w": 8, "x": 16, "y": 21},
) )
run_mix_panel["targets"][0]["legendFormat"] = "{{status}}" run_mix_panel["targets"][0]["legendFormat"] = "{{status}}"
run_mix_panel["fieldConfig"]["defaults"]["unit"] = "none" run_mix_panel["fieldConfig"]["defaults"]["unit"] = "none"
panels.append(run_mix_panel) panels.append(run_mix_panel)
panels.append( check_dimensions = [
timeseries_panel( ("Tests", check_regex_tests),
130, ("Coverage", check_regex_coverage),
"Fail Trend: Tests", ("LOC", check_regex_loc),
_check_state_series(check_regex_tests, True), ("Style", check_regex_style),
{"h": 6, "w": 3, "x": 0, "y": 33}, ("Gate Glue", check_regex_gate_glue),
unit="none", ("SonarQube", check_regex_sonarqube),
legend="{{suite}}", ("Supply Chain", check_regex_supply_chain),
legend_display="list", ]
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"], def _append_check_trends(start_id: int, title_prefix: str, failed: bool, y: int) -> None:
) for index, (label, regex) in enumerate(check_dimensions[:4]):
) panel = timeseries_panel(
panels.append( start_id + index,
timeseries_panel( f"{title_prefix}: {label}",
131, _check_state_series(regex, failed),
"Fail Trend: Coverage", {"h": 7, "w": 6, "x": index * 6, "y": y},
_check_state_series(check_regex_coverage, True), unit="none",
{"h": 6, "w": 3, "x": 3, "y": 33}, legend="{{suite}}",
unit="none", legend_display="list",
legend="{{suite}}", legend_placement="bottom",
legend_display="list", legend_calcs=["lastNotNull", "max"],
legend_placement="bottom", )
legend_calcs=["lastNotNull", "max"], panel["description"] = (
) "One line per selected suite. 1 means this check dimension was in that state during the bucket; "
) "0 means the suite reported the dimension and it was not in that state."
panels.append( )
timeseries_panel( panels.append(panel)
132, for index, (label, regex) in enumerate(check_dimensions[4:]):
"Fail Trend: LOC", panel = timeseries_panel(
_check_state_series(check_regex_loc, True), start_id + 4 + index,
{"h": 6, "w": 3, "x": 6, "y": 33}, f"{title_prefix}: {label}",
unit="none", _check_state_series(regex, failed),
legend="{{suite}}", {"h": 7, "w": 8, "x": index * 8, "y": y + 7},
legend_display="list", unit="none",
legend_placement="bottom", legend="{{suite}}",
legend_calcs=["lastNotNull", "max"], legend_display="list",
) legend_placement="bottom",
) legend_calcs=["lastNotNull", "max"],
panels.append( )
timeseries_panel( panel["description"] = (
133, "One line per selected suite. 1 means this check dimension was in that state during the bucket; "
"Fail Trend: Style", "0 means the suite reported the dimension and it was not in that state."
_check_state_series(check_regex_style, True), )
{"h": 6, "w": 3, "x": 9, "y": 33}, panels.append(panel)
unit="none",
legend="{{suite}}", _append_check_trends(130, "Failure Trend", True, 29)
legend_display="list", _append_check_trends(138, "Success Trend", False, 43)
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
134,
"Fail Trend: Gate Glue",
_check_state_series(check_regex_gate_glue, True),
{"h": 6, "w": 3, "x": 12, "y": 33},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
135,
"Fail Trend: SonarQube",
_check_state_series(check_regex_sonarqube, True),
{"h": 6, "w": 3, "x": 15, "y": 33},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
136,
"Fail Trend: Supply Chain",
_check_state_series(check_regex_supply_chain, True),
{"h": 6, "w": 3, "x": 18, "y": 33},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
138,
"Pass Trend: Tests",
_check_state_series(check_regex_tests, False),
{"h": 6, "w": 3, "x": 0, "y": 39},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
139,
"Pass Trend: Coverage",
_check_state_series(check_regex_coverage, False),
{"h": 6, "w": 3, "x": 3, "y": 39},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
140,
"Pass Trend: LOC",
_check_state_series(check_regex_loc, False),
{"h": 6, "w": 3, "x": 6, "y": 39},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
141,
"Pass Trend: Style",
_check_state_series(check_regex_style, False),
{"h": 6, "w": 3, "x": 9, "y": 39},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
142,
"Pass Trend: Gate Glue",
_check_state_series(check_regex_gate_glue, False),
{"h": 6, "w": 3, "x": 12, "y": 39},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
143,
"Pass Trend: SonarQube",
_check_state_series(check_regex_sonarqube, False),
{"h": 6, "w": 3, "x": 15, "y": 39},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
timeseries_panel(
144,
"Pass Trend: Supply Chain",
_check_state_series(check_regex_supply_chain, False),
{"h": 6, "w": 3, "x": 18, "y": 39},
unit="none",
legend="{{suite}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "max"],
)
)
panels.append(
bargauge_panel(
15,
"Latest Test Counters (Suite + Result)",
f'sum by (suite, result) ({{{tests_selector}}})',
{"h": 6, "w": 3, "x": 21, "y": 39},
unit="none",
instant=True,
legend="{{suite}} · {{result}}",
sort_order="desc",
limit=24,
)
)
panels.append( panels.append(
timeseries_panel( timeseries_panel(
145, 145,
"Problematic Tests Over Time (Top failures)", "Problematic Tests Over Time (Top failures)",
problematic_tests_history, problematic_tests_history,
{"h": 8, "w": 12, "x": 0, "y": 45}, {"h": 8, "w": 12, "x": 0, "y": 57},
unit="none", unit="none",
legend="{{suite}} · {{test}}", legend="{{suite}} · {{test}}",
legend_display="list", legend_display="list",
@ -3767,27 +3544,12 @@ def build_jobs_dashboard():
data_links=jenkins_latest_artifact_data_links(), data_links=jenkins_latest_artifact_data_links(),
) )
) )
panels.append(
timeseries_panel(
146,
"Selected Test Pass/Fail History",
None,
{"h": 8, "w": 8, "x": 12, "y": 45},
unit="none",
targets=selected_test_pass_fail,
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "sum"],
links=jenkins_suite_links(),
data_links=jenkins_artifact_data_links(),
)
)
panels.append( panels.append(
bargauge_panel( bargauge_panel(
147, 147,
"Most Problematic Test by Suite (30d)", "Most Problematic Test by Suite (30d)",
worst_test_per_suite, worst_test_per_suite,
{"h": 8, "w": 4, "x": 20, "y": 45}, {"h": 8, "w": 12, "x": 12, "y": 57},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}} · {{test}}", legend="{{suite}} · {{test}}",
@ -3798,17 +3560,49 @@ def build_jobs_dashboard():
data_links=jenkins_latest_artifact_data_links(), data_links=jenkins_latest_artifact_data_links(),
) )
) )
panels.append(
timeseries_panel(
146,
"Selected Test Pass/Fail History",
None,
{"h": 8, "w": 12, "x": 0, "y": 65},
unit="none",
targets=selected_test_pass_fail,
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "sum"],
links=jenkins_suite_links(),
data_links=jenkins_artifact_data_links(),
)
)
selected_pass_rate_panel = timeseries_panel(
152,
"Selected Test Pass Rate History",
selected_test_pass_rate,
{"h": 8, "w": 12, "x": 12, "y": 65},
unit="percent",
legend="{{suite}} · {{test}}",
legend_display="list",
legend_placement="bottom",
legend_calcs=["lastNotNull", "min"],
links=jenkins_suite_links(),
data_links=jenkins_artifact_data_links(),
)
selected_pass_rate_panel["fieldConfig"]["defaults"]["min"] = 0
selected_pass_rate_panel["fieldConfig"]["defaults"]["max"] = 100
selected_pass_rate_panel["fieldConfig"]["defaults"]["thresholds"] = success_thresholds
panels.append(selected_pass_rate_panel)
coverage_panel = bargauge_panel( coverage_panel = bargauge_panel(
17, 17,
"Coverage by Suite (Latest, gate 95)", "Coverage by Suite (Latest, gate 95)",
coverage_with_missing, coverage_with_missing,
{"h": 8, "w": 12, "x": 0, "y": 53}, {"h": 8, "w": 12, "x": 0, "y": 73},
unit="percent", unit="percent",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
sort_order="asc", sort_order="asc",
thresholds=success_thresholds, thresholds=coverage_thresholds,
decimals=2, decimals=2,
) )
coverage_panel["fieldConfig"]["defaults"]["mappings"] = [ coverage_panel["fieldConfig"]["defaults"]["mappings"] = [
@ -3820,7 +3614,7 @@ def build_jobs_dashboard():
18, 18,
"Files >500 LOC by Suite (Latest)", "Files >500 LOC by Suite (Latest)",
smell_with_missing, smell_with_missing,
{"h": 8, "w": 12, "x": 12, "y": 53}, {"h": 8, "w": 12, "x": 12, "y": 73},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3837,7 +3631,7 @@ def build_jobs_dashboard():
27, 27,
"Missing Tests Metrics by Suite", "Missing Tests Metrics by Suite",
missing_tests_by_suite, missing_tests_by_suite,
{"h": 7, "w": 6, "x": 0, "y": 61}, {"h": 7, "w": 6, "x": 0, "y": 81},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3851,7 +3645,7 @@ def build_jobs_dashboard():
28, 28,
"Missing Checks Metrics by Suite", "Missing Checks Metrics by Suite",
missing_checks_by_suite, missing_checks_by_suite,
{"h": 7, "w": 6, "x": 6, "y": 61}, {"h": 7, "w": 6, "x": 6, "y": 81},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3865,7 +3659,7 @@ def build_jobs_dashboard():
29, 29,
"Missing Coverage Metrics by Suite", "Missing Coverage Metrics by Suite",
missing_coverage_by_suite, missing_coverage_by_suite,
{"h": 7, "w": 6, "x": 12, "y": 61}, {"h": 7, "w": 6, "x": 12, "y": 81},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3879,7 +3673,7 @@ def build_jobs_dashboard():
30, 30,
"Missing LOC Metrics by Suite", "Missing LOC Metrics by Suite",
missing_loc_by_suite, missing_loc_by_suite,
{"h": 7, "w": 6, "x": 18, "y": 61}, {"h": 7, "w": 6, "x": 18, "y": 81},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3893,7 +3687,7 @@ def build_jobs_dashboard():
31, 31,
"SonarQube API Up", "SonarQube API Up",
"(max(sonarqube_up) or on() vector(0))", "(max(sonarqube_up) or on() vector(0))",
{"h": 6, "w": 4, "x": 0, "y": 68}, {"h": 6, "w": 4, "x": 0, "y": 88},
unit="none", unit="none",
instant=True, instant=True,
thresholds={ thresholds={
@ -3910,7 +3704,7 @@ def build_jobs_dashboard():
32, 32,
"Sonar Projects (Selected)", "Sonar Projects (Selected)",
f'(count(sonarqube_project_quality_gate_pass{{project_key=~"{suite_var}"}}) or on() vector(0))', f'(count(sonarqube_project_quality_gate_pass{{project_key=~"{suite_var}"}}) or on() vector(0))',
{"h": 6, "w": 4, "x": 4, "y": 68}, {"h": 6, "w": 4, "x": 4, "y": 88},
unit="none", unit="none",
instant=True, instant=True,
thresholds=failures_thresholds, thresholds=failures_thresholds,
@ -3921,7 +3715,7 @@ def build_jobs_dashboard():
33, 33,
"Sonar Gate Fetch Errors", "Sonar Gate Fetch Errors",
"(max(sonarqube_quality_gate_fetch_errors_total) or on() vector(0))", "(max(sonarqube_quality_gate_fetch_errors_total) or on() vector(0))",
{"h": 6, "w": 4, "x": 8, "y": 68}, {"h": 6, "w": 4, "x": 8, "y": 88},
unit="none", unit="none",
instant=True, instant=True,
thresholds=failures_thresholds, thresholds=failures_thresholds,
@ -3931,7 +3725,7 @@ def build_jobs_dashboard():
34, 34,
"Sonar Gate Status Mix (Selected)", "Sonar Gate Status Mix (Selected)",
f'count by (status) (sonarqube_project_quality_gate_pass{{project_key=~"{suite_var}"}})', f'count by (status) (sonarqube_project_quality_gate_pass{{project_key=~"{suite_var}"}})',
{"h": 6, "w": 6, "x": 12, "y": 68}, {"h": 6, "w": 6, "x": 12, "y": 88},
) )
sonar_status_mix_panel["targets"][0]["legendFormat"] = "{{status}}" sonar_status_mix_panel["targets"][0]["legendFormat"] = "{{status}}"
panels.append(sonar_status_mix_panel) panels.append(sonar_status_mix_panel)
@ -3941,7 +3735,7 @@ def build_jobs_dashboard():
"Projects Failing Sonar Gate", "Projects Failing Sonar Gate",
f'(sort_desc(count by (project_key) (sonarqube_project_quality_gate_pass{{project_key=~"{suite_var}",status!~"OK|ok"}})) ' f'(sort_desc(count by (project_key) (sonarqube_project_quality_gate_pass{{project_key=~"{suite_var}",status!~"OK|ok"}})) '
f'or on() label_replace(vector(0), "project_key", "none", "__name__", ".*"))', f'or on() label_replace(vector(0), "project_key", "none", "__name__", ".*"))',
{"h": 6, "w": 6, "x": 18, "y": 68}, {"h": 6, "w": 6, "x": 18, "y": 88},
unit="none", unit="none",
instant=True, instant=True,
legend="{{project_key}}", legend="{{project_key}}",
@ -3954,7 +3748,21 @@ def build_jobs_dashboard():
148, 148,
"Missing Test-Case Metrics by Suite", "Missing Test-Case Metrics by Suite",
missing_test_case_by_suite, missing_test_case_by_suite,
{"h": 6, "w": 24, "x": 0, "y": 74}, {"h": 6, "w": 12, "x": 0, "y": 94},
unit="none",
instant=True,
legend="{{suite}}",
sort_order="desc",
thresholds=missing_thresholds,
decimals=0,
)
)
panels.append(
bargauge_panel(
151,
"No Real Test Cases by Suite",
placeholder_test_case_by_suite,
{"h": 6, "w": 12, "x": 12, "y": 94},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}}", legend="{{suite}}",
@ -3968,7 +3776,7 @@ def build_jobs_dashboard():
149, 149,
"Recent Branch Evidence by Suite (30d)", "Recent Branch Evidence by Suite (30d)",
recent_branch_evidence, recent_branch_evidence,
{"h": 7, "w": 12, "x": 0, "y": 80}, {"h": 7, "w": 12, "x": 0, "y": 100},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}} · {{branch}}", legend="{{suite}} · {{branch}}",
@ -3983,7 +3791,7 @@ def build_jobs_dashboard():
150, 150,
"Non-Primary Branch Evidence (30d)", "Non-Primary Branch Evidence (30d)",
non_primary_branch_evidence, non_primary_branch_evidence,
{"h": 7, "w": 12, "x": 12, "y": 80}, {"h": 7, "w": 12, "x": 12, "y": 100},
unit="none", unit="none",
instant=True, instant=True,
legend="{{suite}} · {{branch}}", legend="{{suite}} · {{branch}}",

View File

@ -182,6 +182,56 @@ EOF
} }
} }
} }
stage('Validation tests') {
steps {
container('git') {
sh '''#!/usr/bin/env sh
set -eu
mkdir -p build
failures=0
cases=""
add_case() {
name="$1"
message="$2"
if [ -n "${message}" ]; then
failures=$((failures + 1))
cases="${cases}<testcase classname=\"data_prepper.packaging\" name=\"${name}\"><failure message=\"${message}\" /></testcase>"
else
cases="${cases}<testcase classname=\"data_prepper.packaging\" name=\"${name}\" />"
fi
}
if [ -s dockerfiles/Dockerfile.data-prepper ]; then
add_case "dockerfile_present" ""
else
add_case "dockerfile_present" "dockerfiles/Dockerfile.data-prepper is missing or empty"
fi
if [ -s services/logging/scripts/data_prepper_pipelines.yaml ]; then
add_case "pipeline_config_present" ""
else
add_case "pipeline_config_present" "data_prepper_pipelines.yaml is missing or empty"
fi
if grep -q 'data-prepper-helmrelease.yaml' services/logging/kustomization.yaml; then
add_case "logging_kustomization_includes_data_prepper" ""
else
add_case "logging_kustomization_includes_data_prepper" "services/logging/kustomization.yaml does not include data-prepper HelmRelease"
fi
cat > build/junit-data-prepper.xml <<EOF
<testsuite name="data_prepper.packaging" tests="3" failures="${failures}" errors="0" skipped="0">
${cases}
</testsuite>
EOF
if [ "${failures}" -ne 0 ]; then
exit 1
fi
'''
}
}
}
stage('Enforce quality gate') { stage('Enforce quality gate') {
steps { steps {
container('git') { container('git') {
@ -290,7 +340,7 @@ EOF
container('git') { container('git') {
sh ''' sh '''
set -euo pipefail set -euo pipefail
apk add --no-cache curl jq >/dev/null 2>&1 || true apk add --no-cache curl jq python3 >/dev/null 2>&1 || true
suite="${SUITE_NAME}" suite="${SUITE_NAME}"
gateway="${PUSHGATEWAY_URL}" gateway="${PUSHGATEWAY_URL}"
status="${QUALITY_OUTCOME:-failed}" status="${QUALITY_OUTCOME:-failed}"
@ -350,16 +400,90 @@ EOF
metric_branch="$(printf '%s' "${metric_branch_raw}" | jq -Rsa . | sed -e 's/^"//' -e 's/"$//')" metric_branch="$(printf '%s' "${metric_branch_raw}" | jq -Rsa . | sed -e 's/^"//' -e 's/"$//')"
metric_build_number="$(printf '%s' "${BUILD_NUMBER:-unknown}" | jq -Rsa . | sed -e 's/^"//' -e 's/"$//')" metric_build_number="$(printf '%s' "${BUILD_NUMBER:-unknown}" | jq -Rsa . | sed -e 's/^"//' -e 's/"$//')"
metric_jenkins_job="$(printf '%s' "${JOB_NAME:-data-prepper}" | jq -Rsa . | sed -e 's/^"//' -e 's/"$//')" metric_jenkins_job="$(printf '%s' "${JOB_NAME:-data-prepper}" | jq -Rsa . | sed -e 's/^"//' -e 's/"$//')"
cat <<METRICS | curl -fsS -X PUT --data-binary @- "${gateway}/metrics/job/platform-quality-ci/suite/${suite}" >/dev/null || \ export METRIC_SUITE="${suite}"
echo "warning: metrics push failed for suite=${suite}" >&2 export METRIC_BRANCH_RAW="${metric_branch_raw}"
export METRIC_BUILD_NUMBER_RAW="${BUILD_NUMBER:-unknown}"
export METRIC_JENKINS_JOB_RAW="${JOB_NAME:-data-prepper}"
python3 - <<'PY'
import glob
import os
import xml.etree.ElementTree as ET
from pathlib import Path
def label_value(value: str) -> str:
return value.replace("\\", "\\\\").replace("\n", "\\n").replace('"', '\\"')
totals = {"passed": 0, "failed": 0, "error": 0, "skipped": 0}
case_lines = []
suite = os.environ["METRIC_SUITE"]
branch = os.environ["METRIC_BRANCH_RAW"]
build_number = os.environ["METRIC_BUILD_NUMBER_RAW"]
jenkins_job = os.environ["METRIC_JENKINS_JOB_RAW"]
for path in glob.glob("build/junit-*.xml"):
try:
root = ET.parse(path).getroot()
except ET.ParseError:
totals["error"] += 1
continue
for case in root.findall(".//testcase"):
name = case.get("name") or "unnamed"
classname = case.get("classname") or Path(path).stem
test_name = f"{classname}::{name}" if classname else name
status = "passed"
if case.find("error") is not None:
status = "error"
elif case.find("failure") is not None:
status = "failed"
elif case.find("skipped") is not None:
status = "skipped"
totals[status] += 1
labels = {
"suite": suite,
"branch": branch,
"build_number": build_number,
"jenkins_job": jenkins_job,
"test": test_name,
"status": status,
}
label_blob = ",".join(f'{key}="{label_value(value)}"' for key, value in labels.items())
case_lines.append(f"platform_quality_gate_test_case_result{{{label_blob}}} 1")
if not case_lines:
totals["skipped"] += 1
labels = {
"suite": suite,
"branch": branch,
"build_number": build_number,
"jenkins_job": jenkins_job,
"test": "__no_test_cases__",
"status": "skipped",
}
label_blob = ",".join(f'{key}="{label_value(value)}"' for key, value in labels.items())
case_lines.append(f"platform_quality_gate_test_case_result{{{label_blob}}} 1")
Path("build/test-counts.env").write_text(
"\n".join(f"test_{key}_count={value}" for key, value in totals.items()) + "\n",
encoding="utf-8",
)
Path("build/testcase-metrics.prom").write_text("\n".join(case_lines) + "\n", encoding="utf-8")
PY
. build/test-counts.env
tests_check="ok"
if [ "$((test_failed_count + test_error_count))" -gt 0 ]; then
tests_check="failed"
fi
cat > build/platform-quality-metrics.prom <<METRICS
# TYPE platform_quality_gate_runs_total counter # TYPE platform_quality_gate_runs_total counter
platform_quality_gate_runs_total{suite="${suite}",status="ok"} ${ok_count} platform_quality_gate_runs_total{suite="${suite}",status="ok"} ${ok_count}
platform_quality_gate_runs_total{suite="${suite}",status="failed"} ${failed_count} platform_quality_gate_runs_total{suite="${suite}",status="failed"} ${failed_count}
# TYPE data_prepper_quality_gate_tests_total gauge # TYPE data_prepper_quality_gate_tests_total gauge
data_prepper_quality_gate_tests_total{suite="${suite}",result="passed"} 0 data_prepper_quality_gate_tests_total{suite="${suite}",result="passed"} ${test_passed_count}
data_prepper_quality_gate_tests_total{suite="${suite}",result="failed"} 0 data_prepper_quality_gate_tests_total{suite="${suite}",result="failed"} ${test_failed_count}
data_prepper_quality_gate_tests_total{suite="${suite}",result="error"} 0 data_prepper_quality_gate_tests_total{suite="${suite}",result="error"} ${test_error_count}
data_prepper_quality_gate_tests_total{suite="${suite}",result="skipped"} 0 data_prepper_quality_gate_tests_total{suite="${suite}",result="skipped"} ${test_skipped_count}
# TYPE platform_quality_gate_workspace_line_coverage_percent gauge # TYPE platform_quality_gate_workspace_line_coverage_percent gauge
# No coverable project source is present in this packaging suite; report full # No coverable project source is present in this packaging suite; report full
# non-applicable coverage so rollups do not confuse N/A with uncovered code. # non-applicable coverage so rollups do not confuse N/A with uncovered code.
@ -369,7 +493,7 @@ platform_quality_gate_source_lines_over_500_total{suite="${suite}"} 0
# TYPE platform_quality_gate_build_info gauge # TYPE platform_quality_gate_build_info gauge
platform_quality_gate_build_info{suite="${suite}",branch="${metric_branch}",build_number="${metric_build_number}",jenkins_job="${metric_jenkins_job}"} 1 platform_quality_gate_build_info{suite="${suite}",branch="${metric_branch}",build_number="${metric_build_number}",jenkins_job="${metric_jenkins_job}"} 1
# TYPE data_prepper_quality_gate_checks_total gauge # TYPE data_prepper_quality_gate_checks_total gauge
data_prepper_quality_gate_checks_total{suite="${suite}",check="tests",result="not_applicable"} 1 data_prepper_quality_gate_checks_total{suite="${suite}",check="tests",result="${tests_check}"} 1
data_prepper_quality_gate_checks_total{suite="${suite}",check="coverage",result="not_applicable"} 1 data_prepper_quality_gate_checks_total{suite="${suite}",check="coverage",result="not_applicable"} 1
data_prepper_quality_gate_checks_total{suite="${suite}",check="loc",result="not_applicable"} 1 data_prepper_quality_gate_checks_total{suite="${suite}",check="loc",result="not_applicable"} 1
data_prepper_quality_gate_checks_total{suite="${suite}",check="docs_naming",result="not_applicable"} 1 data_prepper_quality_gate_checks_total{suite="${suite}",check="docs_naming",result="not_applicable"} 1
@ -377,10 +501,17 @@ data_prepper_quality_gate_checks_total{suite="${suite}",check="gate_glue",result
data_prepper_quality_gate_checks_total{suite="${suite}",check="sonarqube",result="${sonarqube_check}"} 1 data_prepper_quality_gate_checks_total{suite="${suite}",check="sonarqube",result="${sonarqube_check}"} 1
data_prepper_quality_gate_checks_total{suite="${suite}",check="supply_chain",result="${supply_chain_check}"} 1 data_prepper_quality_gate_checks_total{suite="${suite}",check="supply_chain",result="${supply_chain_check}"} 1
# TYPE platform_quality_gate_test_case_result gauge # TYPE platform_quality_gate_test_case_result gauge
platform_quality_gate_test_case_result{suite="${suite}",branch="${metric_branch}",build_number="${metric_build_number}",jenkins_job="${metric_jenkins_job}",test="__no_test_cases__",status="skipped"} 1
METRICS METRICS
cat build/testcase-metrics.prom >> build/platform-quality-metrics.prom
curl -fsS -X PUT --data-binary @build/platform-quality-metrics.prom "${gateway}/metrics/job/platform-quality-ci/suite/${suite}" >/dev/null || \
echo "warning: metrics push failed for suite=${suite}" >&2
''' '''
} }
script {
if (fileExists('build/junit-data-prepper.xml')) {
junit allowEmptyResults: true, testResults: 'build/junit-*.xml'
}
}
archiveArtifacts artifacts: 'build/**', allowEmptyArchive: true, fingerprint: true archiveArtifacts artifacts: 'build/**', allowEmptyArchive: true, fingerprint: true
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff