Skip to content

Commit 48f2d2c

Browse files
authored
Bundle raw ya trace inputs
1 parent 5d6c2fa commit 48f2d2c

5 files changed

Lines changed: 430 additions & 0 deletions

File tree

.github/scripts/tracing/YA_TRACE_TO_OTLP.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,31 @@ prefixes remain a fallback for older OTLP bundles. Only HTTP(S) prefixes without
681681
credentials, queries, fragments, or NUL bytes are retained; relative artifact
682682
paths are validated and encoded in the browser before a link is created.
683683

684+
## Raw-input bundle and nonblocking CI behavior
685+
686+
Python writes only `trace-inputs.files`, a NUL-delimited list of selected trace
687+
paths relative to `ya-out`. It never invokes `tar`.
688+
689+
[`pack_ya_trace_inputs.sh`](pack_ya_trace_inputs.sh) creates
690+
`trace-inputs.tar.gz` in shell code. It uses `--null` and
691+
`--verbatim-files-from` so newlines, leading dashes, and other unusual path
692+
characters are treated literally. Trace files are stored below `ya-out/`; the
693+
event log is stored at the archive root. No separate raw-input manifest is
694+
needed because conversion metadata already lives in the OTLP/report manifest.
695+
696+
[`render_ya_trace_bundle.sh`](render_ya_trace_bundle.sh) is the shared command
697+
used by both build and test actions. It:
698+
699+
1. removes stale trace outputs;
700+
2. runs the Python converter;
701+
3. runs the shell packer even if Python failed;
702+
4. emits GitHub warnings, links, and outputs according to available files;
703+
5. returns success for renderer/packer failures so diagnostics cannot change
704+
the build/test result.
705+
706+
If Python fails before writing its path list, the packer independently finds
707+
`ytest.report.trace` files so the raw evidence can still be uploaded.
708+
684709
## Running the converter directly
685710

686711
Install requirements and expose `.github` as the Python package root:
@@ -707,3 +732,22 @@ python3 -m scripts.tracing.ya_trace_report \
707732
--retry 1 \
708733
--operation tests
709734
```
735+
736+
Or use the same nonblocking bundle command as CI:
737+
738+
```bash
739+
bash .github/scripts/tracing/render_ya_trace_bundle.sh \
740+
--report-dir /path/to/trace-summary \
741+
--ya-out /path/to/ya-out \
742+
--evlog /path/to/ya_evlog.jsonl \
743+
--warning-title "Trace report" \
744+
--html-url https://artifacts.example/trace.html \
745+
--otlp-url https://artifacts.example/trace.otlp.jsonl.gz \
746+
--inputs-url https://artifacts.example/trace-inputs.tar.gz \
747+
-- \
748+
--attempt-start-ns 1753952400000000000 \
749+
--attempt-end-ns 1753952700000000000 \
750+
--exit-code 0 \
751+
--component cloud/blockstore \
752+
--operation tests
753+
```
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
5+
echo "Usage: $0 REPORT_DIR YA_OUT [EVLOG]" >&2
6+
exit 2
7+
fi
8+
9+
report_dir=$1
10+
ya_out=$2
11+
evlog=${3:-}
12+
13+
if [ -e "$report_dir" ] && [ ! -d "$report_dir" ]; then
14+
echo "Trace report path is not a directory: $report_dir" >&2
15+
exit 2
16+
fi
17+
if [ ! -d "$ya_out" ]; then
18+
echo "Ya output directory does not exist: $ya_out" >&2
19+
exit 2
20+
fi
21+
22+
mkdir -p -- "$report_dir"
23+
report_dir=$(realpath -- "$report_dir")
24+
ya_out=$(realpath -- "$ya_out")
25+
file_list="$report_dir/trace-inputs.files"
26+
output="$report_dir/trace-inputs.tar.gz"
27+
temporary=
28+
29+
cleanup() {
30+
[ -z "$temporary" ] || rm -f -- "$temporary"
31+
if [ ! -d "$file_list" ] || [ -L "$file_list" ]; then
32+
rm -f -- "$file_list"
33+
fi
34+
}
35+
trap cleanup EXIT
36+
37+
rm -f -- "$output"
38+
if [ -e "$file_list" ] || [ -L "$file_list" ]; then
39+
if [ ! -f "$file_list" ] || [ -L "$file_list" ]; then
40+
echo "Trace input file list is unsafe: $file_list" >&2
41+
exit 2
42+
fi
43+
else
44+
# Rendering may fail before Python can emit its selected paths (for
45+
# example, when an optional tracing dependency is unavailable).
46+
find "$ya_out" -type f -name ytest.report.trace -printf './%P\0' > "$file_list"
47+
fi
48+
49+
include_evlog=0
50+
if [ -n "$evlog" ] && [ -f "$evlog" ] && [ ! -L "$evlog" ]; then
51+
include_evlog=1
52+
fi
53+
if [ ! -s "$file_list" ] && [ "$include_evlog" -eq 0 ]; then
54+
exit 0
55+
fi
56+
57+
temporary=$(mktemp "$report_dir/.trace-inputs.XXXXXX.tar.gz.tmp")
58+
tar_args=(
59+
--create
60+
"--file=$temporary"
61+
--gzip
62+
# Archive exactly the Python-selected files even if one becomes a directory.
63+
--no-recursion
64+
--null
65+
# Read NUL-delimited paths literally. In particular, do not unquote names
66+
# or interpret a leading '-' as another tar option.
67+
--verbatim-files-from
68+
--owner=0
69+
--group=0
70+
--numeric-owner
71+
'--mode=u=rw,go=r'
72+
# Python-listed paths start with "./"; store them below ya-out/.
73+
'--transform=s|^\./|ya-out/|'
74+
)
75+
76+
if [ "$include_evlog" -eq 1 ]; then
77+
evlog=$(realpath -- "$evlog")
78+
tar_args+=(
79+
"--directory=$(dirname "$evlog")"
80+
"--add-file=$(basename "$evlog")"
81+
)
82+
fi
83+
84+
tar_args+=(
85+
"--directory=$ya_out"
86+
"--files-from=$file_list"
87+
)
88+
89+
tar "${tar_args[@]}"
90+
mv "$temporary" "$output"
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import subprocess
2+
import tarfile
3+
from pathlib import Path
4+
5+
PACK_SCRIPT = Path(__file__).with_name("pack_ya_trace_inputs.sh")
6+
7+
8+
def _trace_path(ya_out: Path, suite: str) -> Path:
9+
return ya_out / suite / "test-results" / "unittest" / "ytest.report.trace"
10+
11+
12+
def test_pack_uses_python_selected_paths_without_auxiliary_manifest(
13+
tmp_path: Path,
14+
) -> None:
15+
report_dir = tmp_path / "summary"
16+
ya_out = tmp_path / "out"
17+
selected = _trace_path(ya_out, "selected")
18+
ignored = _trace_path(ya_out, "ignored")
19+
for path in (selected, ignored):
20+
path.parent.mkdir(parents=True)
21+
path.write_text(path.parent.parent.parent.name)
22+
report_dir.mkdir()
23+
(report_dir / "trace-inputs.files").write_bytes(
24+
b"./selected/test-results/unittest/ytest.report.trace\0"
25+
)
26+
evlog = tmp_path / "ya_evlog.jsonl"
27+
evlog.write_text("{}\n")
28+
29+
subprocess.run(["bash", PACK_SCRIPT, report_dir, ya_out, evlog], check=True)
30+
31+
with tarfile.open(report_dir / "trace-inputs.tar.gz") as archive:
32+
assert set(archive.getnames()) == {
33+
"ya_evlog.jsonl",
34+
"ya-out/selected/test-results/unittest/ytest.report.trace",
35+
}
36+
assert not (report_dir / "trace-inputs.files").exists()
37+
38+
39+
def test_pack_discovers_inputs_after_renderer_startup_failure(tmp_path: Path) -> None:
40+
report_dir = tmp_path / "summary"
41+
ya_out = tmp_path / "out"
42+
trace = _trace_path(ya_out, "suite")
43+
trace.parent.mkdir(parents=True)
44+
trace.write_text("{}\n")
45+
(ya_out / "not-a-trace").write_text("ignored")
46+
47+
subprocess.run(["bash", PACK_SCRIPT, report_dir, ya_out], check=True)
48+
49+
with tarfile.open(report_dir / "trace-inputs.tar.gz") as archive:
50+
assert archive.getnames() == [
51+
"ya-out/suite/test-results/unittest/ytest.report.trace"
52+
]
53+
54+
55+
def test_pack_treats_unusual_python_selected_paths_literally(tmp_path: Path) -> None:
56+
report_dir = tmp_path / "summary"
57+
ya_out = tmp_path / "out"
58+
trace = _trace_path(ya_out, "-suite\nwith-newline")
59+
trace.parent.mkdir(parents=True)
60+
trace.write_text("{}\n")
61+
report_dir.mkdir()
62+
relative = trace.relative_to(ya_out).as_posix()
63+
(report_dir / "trace-inputs.files").write_bytes(f"./{relative}".encode() + b"\0")
64+
65+
subprocess.run(["bash", PACK_SCRIPT, report_dir, ya_out], check=True)
66+
67+
with tarfile.open(report_dir / "trace-inputs.tar.gz") as archive:
68+
assert archive.getnames() == [f"ya-out/{relative}"]
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
echo "Usage: $0 --report-dir DIR --ya-out DIR [options] -- REPORT_OPTIONS..." >&2
6+
exit 2
7+
}
8+
9+
report_dir=
10+
ya_out=
11+
evlog=
12+
warning_title="Trace report"
13+
html_url=
14+
otlp_url=
15+
inputs_url=
16+
summary=
17+
summary_heading=
18+
github_output=
19+
20+
while [ "$#" -gt 0 ]; do
21+
case "$1" in
22+
--report-dir)
23+
report_dir=${2:-}
24+
shift 2
25+
;;
26+
--ya-out)
27+
ya_out=${2:-}
28+
shift 2
29+
;;
30+
--evlog)
31+
evlog=${2:-}
32+
shift 2
33+
;;
34+
--warning-title)
35+
warning_title=${2:-}
36+
shift 2
37+
;;
38+
--html-url)
39+
html_url=${2:-}
40+
shift 2
41+
;;
42+
--otlp-url)
43+
otlp_url=${2:-}
44+
shift 2
45+
;;
46+
--inputs-url)
47+
inputs_url=${2:-}
48+
shift 2
49+
;;
50+
--summary)
51+
summary=${2:-}
52+
shift 2
53+
;;
54+
--summary-heading)
55+
summary_heading=${2:-}
56+
shift 2
57+
;;
58+
--github-output)
59+
github_output=${2:-}
60+
shift 2
61+
;;
62+
--)
63+
shift
64+
break
65+
;;
66+
*) usage ;;
67+
esac
68+
done
69+
if [ -z "$report_dir" ] || [ -z "$ya_out" ]; then
70+
usage
71+
fi
72+
73+
mkdir -p -- "$report_dir"
74+
rm -f -- \
75+
"$report_dir/trace.html" \
76+
"$report_dir/trace.manifest.json" \
77+
"$report_dir/trace.otlp.jsonl.gz" \
78+
"$report_dir/trace-inputs.tar.gz"
79+
80+
report_args=(--ya-out "$ya_out" --output-dir "$report_dir")
81+
if [ -n "$evlog" ]; then
82+
report_args+=(--evlog "$evlog")
83+
fi
84+
85+
if ! python3 -m scripts.tracing.ya_trace_report "${report_args[@]}" "$@"; then
86+
echo "::warning title=$warning_title::Unable to generate the ya OTLP trace bundle"
87+
fi
88+
89+
script_dir=$(dirname "$(realpath -- "$0")")
90+
if ! bash "$script_dir/pack_ya_trace_inputs.sh" "$report_dir" "$ya_out" "$evlog"; then
91+
echo "::warning title=$warning_title::Unable to archive the raw ya trace inputs"
92+
fi
93+
94+
report_available=0
95+
inputs_available=0
96+
if [ -f "$report_dir/trace.html" ] && [ -f "$report_dir/trace.otlp.jsonl.gz" ]; then
97+
report_available=1
98+
fi
99+
if [ -f "$report_dir/trace-inputs.tar.gz" ]; then
100+
inputs_available=1
101+
fi
102+
103+
if [ -n "$github_output" ]; then
104+
if [ "$report_available" -eq 1 ]; then
105+
echo "html_url=$html_url" >> "$github_output"
106+
echo "otlp_url=$otlp_url" >> "$github_output"
107+
fi
108+
if [ "$inputs_available" -eq 1 ]; then
109+
echo "inputs_url=$inputs_url" >> "$github_output"
110+
fi
111+
fi
112+
113+
if [ -n "$summary" ]; then
114+
if [ "$report_available" -eq 1 ]; then
115+
if [ "$inputs_available" -eq 1 ]; then
116+
link="[Execution trace]($html_url) ([raw OTLP JSONL]($otlp_url), [raw trace inputs]($inputs_url))"
117+
else
118+
link="[Execution trace]($html_url) ([raw OTLP JSONL]($otlp_url))"
119+
fi
120+
elif [ "$inputs_available" -eq 1 ]; then
121+
link="[Raw trace inputs]($inputs_url)"
122+
else
123+
link=
124+
fi
125+
if [ -n "$link" ]; then
126+
if [ -n "$summary_heading" ]; then
127+
printf '\n### %s\n' "$summary_heading" >> "$summary"
128+
fi
129+
printf '\n%s\n' "$link" >> "$summary"
130+
fi
131+
fi
132+
133+
# Trace collection is diagnostic and must never change the build/test result.
134+
exit 0

0 commit comments

Comments
 (0)