Prow Job Analyze Resource
This skill analyzes the lifecycle of Kubernetes resources during Prow CI job execution by downloading and parsing artifacts from Google Cloud Storage.
When to Use This Skill
Use this skill when the user wants to:
- Debug Prow CI test failures by tracking resource state changes
- Understand when and how a Kubernetes resource was created, modified, or deleted during a test
- Analyze resource lifecycle across audit logs and pod logs from ephemeral test clusters
- Generate interactive HTML reports showing resource events over time
- Search for specific resources (pods, deployments, configmaps, etc.) in Prow job artifacts
Prerequisites
Before starting, verify these prerequisites:
gcloud CLI Installation
gcloud Authentication (Optional)
- The
test-platform-results bucket is publicly accessible
- No authentication is required for read access
- Skip authentication checks
Input Format
The user will provide:
Prow job URL - gcsweb URL containing test-platform-results/
- Example:
https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/30393/pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn/1978913325970362368/
- URL may or may not have trailing slash
Resource specifications - Comma-delimited list in format [namespace:][kind/]name
- Supports regex patterns for matching multiple resources
- Examples:
pod/etcd-0 - pod named etcd-0 in any namespace
openshift-etcd:pod/etcd-0 - pod in specific namespace
etcd-0 - any resource named etcd-0 (no kind filter)
pod/etcd-0,configmap/cluster-config - multiple resources
resource-name-1|resource-name-2 - multiple resources using regex OR
e2e-test-project-api-.* - all resources matching the pattern
Implementation Steps
Step 1: Parse and Validate URL
Extract bucket path
- Find
test-platform-results/ in URL
- Extract everything after it as the GCS bucket relative path
- If not found, error: "URL must contain 'test-platform-results/'"
Extract build_id
- Search for pattern
/(\d{10,})/ in the bucket path
- build_id must be at least 10 consecutive decimal digits
- Handle URLs with or without trailing slash
- If not found, error: "Could not find build ID (10+ digits) in URL"
Extract prowjob name
- Find the path segment immediately preceding build_id
- Example: In
.../pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn/1978913325970362368/
- Prowjob name:
pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn
Construct GCS paths
- Bucket:
test-platform-results
- Base GCS path:
gs://test-platform-results/{bucket-path}/
- Ensure path ends with
/
Step 2: Parse Resource Specifications
For each comma-delimited resource spec:
Parse format [namespace:][kind/]name
- Split on
: to get namespace (optional)
- Split remaining on
/ to get kind (optional) and name (required)
- Store as structured data:
{namespace, kind, name}
Validate
- name is required
- namespace and kind are optional
- Examples:
pod/etcd-0 → {kind: "pod", name: "etcd-0"}
openshift-etcd:pod/etcd-0 → {namespace: "openshift-etcd", kind: "pod", name: "etcd-0"}
etcd-0 → {name: "etcd-0"}
Step 3: Create Working Directory
Check for existing artifacts first
- Check if
.work/prow-job-analyze-resource/{build_id}/logs/ directory exists and has content
- If it exists with content:
- Use AskUserQuestion tool to ask:
- Question: "Artifacts already exist for build {build_id}. Would you like to use the existing download or re-download?"
- Options:
- "Use existing" - Skip to artifact parsing step (Step 6)
- "Re-download" - Continue to clean and re-download
- If user chooses "Re-download":
- Remove all existing content:
rm -rf .work/prow-job-analyze-resource/{build_id}/logs/
- Also remove tmp directory:
rm -rf .work/prow-job-analyze-resource/{build_id}/tmp/
- This ensures clean state before downloading new content
- If user chooses "Use existing":
- Skip directly to Step 6 (Parse Audit Logs)
- Still need to download prowjob.json if it doesn't exist
Create directory structure
mkdir -p .work/prow-job-analyze-resource/{build_id}/logs
mkdir -p .work/prow-job-analyze-resource/{build_id}/tmp
- Use
.work/prow-job-analyze-resource/ as the base directory (already in .gitignore)
- Use build_id as subdirectory name
- Create
logs/ subdirectory for all downloads
- Create
tmp/ subdirectory for temporary files (intermediate JSON, etc.)
- Working directory:
.work/prow-job-analyze-resource/{build_id}/
Step 4: Download and Validate prowjob.json
Download prowjob.json
gcloud storage cp gs://test-platform-results/{bucket-path}/prowjob.json .work/prow-job-analyze-resource/{build_id}/logs/prowjob.json --no-user-output-enabled
Parse and validate
- Read
.work/prow-job-analyze-resource/{build_id}/logs/prowjob.json
- Search for pattern:
--target=([a-zA-Z0-9-]+)
- If not found:
- Display: "This is not a ci-operator job. The prowjob cannot be analyzed by this skill."
- Explain: ci-operator jobs have a --target argument specifying the test target
- Exit skill
Extract target name
- Capture the target value (e.g.,
e2e-aws-ovn)
- Store for constructing gather-extra path
Step 5: Download Audit Logs and Pod Logs
Construct gather-extra paths
- GCS path:
gs://test-platform-results/{bucket-path}/artifacts/{target}/gather-extra/
- Local path:
.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/
Download audit logs
mkdir -p .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs
gcloud storage cp -r gs://test-platform-results/{bucket-path}/artifacts/{target}/gather-extra/artifacts/audit_logs/ .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs/ --no-user-output-enabled
- Create directory first to avoid gcloud errors
- Use
--no-user-output-enabled to suppress progress output
- If directory not found, warn: "No audit logs found. Job may not have completed or audit logging may be disabled."
Download pod logs
mkdir -p .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods
gcloud storage cp -r gs://test-platform-results/{bucket-path}/artifacts/{target}/gather-extra/artifacts/pods/ .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods/ --no-user-output-enabled
- Create directory first to avoid gcloud errors
- Use
--no-user-output-enabled to suppress progress output
- If directory not found, warn: "No pod logs found."
Step 6: Parse Audit Logs and Pod Logs
IMPORTANT: Use the provided Python script parse_all_logs.py from the skill directory to parse both audit logs and pod logs efficiently.
Usage:
python3 plugins/prow-job/skills/prow-job-analyze-resource/parse_all_logs.py <resource_pattern> \
.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs \
.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods \
> .work/prow-job-analyze-resource/{build_id}/tmp/all_entries.json
Resource Pattern Parameter:
- The
<resource_pattern> parameter supports regex patterns
- Use
| (pipe) to search for multiple resources: resource1|resource2|resource3
- Use
.* for wildcards: e2e-test-project-.*
- Simple substring matching still works:
my-namespace
- Examples:
- Single resource:
e2e-test-project-api-pkjxf
- Multiple resources:
e2e-test-project-api-pkjxf|e2e-test-project-api-7zdxx
- Pattern matching:
e2e-test-project-api-.*
Note: The script outputs status messages to stderr which will display as progress. The JSON output to stdout is clean and ready to use.
What the script does:
Find all log files
- Audit logs:
.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs/**/*.log
- Pod logs:
.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods/**/*.log
Parse audit log files (JSONL format)
- Read file line by line
- Each line is a JSON object (JSONL format)
- Parse JSON into object
e
Extract fields from each audit log entry
e.verb - action (get, list, create, update, patch, delete, watch)
e.user.username - user making request
e.responseStatus.code - HTTP response code (integer)
e.objectRef.namespace - namespace (if namespaced)
e.objectRef.resource - lowercase plural kind (e.g., "pods", "configmaps")
e.objectRef.name - resource name
e.requestReceivedTimestamp - ISO 8601 timestamp
Filter matches for each resource spec
- Uses regex matching on
e.objectRef.namespace and e.objectRef.name
- Pattern matches if found in either namespace or name field
- Supports all regex features:
- Pipe operator:
resource1|resource2 matches either resource
- Wildcards:
e2e-test-.* matches all resources starting with e2e-test-
- Character classes:
[abc] matches a, b, or c
- Simple substring matching still works for patterns without regex special chars
- Performance optimization: plain strings use fast substring search
For each audit log match, capture
- Source: "audit"
- Filename: Full path to .log file
- Line number: Line number in file (1-indexed)
- Level: Based on
e.responseStatus.code
- 200-299: "info"
- 400-499: "warn"
- 500-599: "error"
- Timestamp: Parse
e.requestReceivedTimestamp to datetime
- Content: Full JSON line (for expandable details)
- Summary: Generate formatted summary
- Format:
{verb} {resource}/{name} in {namespace} by {username} → HTTP {code}
- Example:
create pod/etcd-0 in openshift-etcd by system:serviceaccount:kube-system:deployment-controller → HTTP 201
Parse pod log files (plain text format)
- Read file line by line
- Each line is plain text (not JSON)
- Search for resource pattern in line content
For each pod log match, capture
- Source: "pod"
- Filename: Full path to .log file
- Line number: Line number in file (1-indexed)
- Level: Detect from glog format or default to "info"
- Glog format:
E0910 11:43:41.153414 ... (E=error, W=warn, I=info, F=fatal→error)
- Non-glog format: default to "info"
- Timestamp: Extract from start of line if present (format:
YYYY-MM-DDTHH:MM:SS.mmmmmmZ)
- Content: Full log line
- Summary: First 200 characters of line (after timestamp if present)
Combine and sort all entries
- Merge audit log entries and pod log entries
- Sort all entries chronologically by timestamp
- Entries without timestamps are placed at the end
Step 7: Generate HTML Report
IMPORTANT: Use the provided Python script generate_html_report.py from the skill directory.
Usage:
python3 plugins/prow-job/skills/prow-job-analyze-resource/generate_html_report.py \
.work/prow-job-analyze-resource/{build_id}/tmp/all_entries.json \
"{prowjob_name}" \
"{build_id}" \
"{target}" \
"{resource_pattern}" \
"{gcsweb_url}"
Resource Pattern Parameter:
- The
{resource_pattern} should be the same pattern used in the parse script
- For single resources:
e2e-test-project-api-pkjxf
- For multiple resources:
e2e-test-project-api-pkjxf|e2e-test-project-api-7zdxx
- The script will parse the pattern to display the searched resources in the HTML header
Output: The script generates .work/prow-job-analyze-resource/{build_id}/{first_resource_name}.html
What the script does:
Determine report filename
- Format:
.work/prow-job-analyze-resource/{build_id}/{resource_name}.html
- Uses the primary resource name for the filename
Sort all entries by timestamp
- Loads audit log entries from JSON
- Sort chronologically (ascending)
- Entries without timestamps go at the end
Calculate timeline bounds
- min_time: Earliest timestamp found
- max_time: Latest timestamp found
- Time range: max_time - min_time
Generate HTML structure
Header Section:
<div class="header">
<h1>Prow Job Resource Lifecycle Analysis</h1>
<div class="metadata">
<p><strong>Prow Job:</strong> {prowjob-name}</p>
<p><strong>Build ID:</strong> {build_id}</p>
<p><strong>gcsweb URL:</strong> <a href="{original-url}">{original-url}</a></p>
<p><strong>Target:</strong> {target}</p>
<p><strong>Resources:</strong> {resource-list}</p>
<p><strong>Total Entries:</strong> {count}</p>
<p><strong>Time Range:</strong> {min_time} to {max_time}</p>
</div>
</div>
Interactive Timeline:
<div class="timeline-container">
<svg id="timeline" width="100%" height="100">
<!-- For each entry, render colored vertical line -->
<line x1="{position}%" y1="0" x2="{position}%" y2="100"
stroke="{color}" stroke-width="2"
class="timeline-event" data-entry-id="{entry-id}"
title="{summary}">
</line>
</svg>
</div>
- Position: Calculate percentage based on timestamp between min_time and max_time
- Color: white/lightgray (info), yellow (warn), red (error)
- Clickable: Jump to corresponding entry
- Tooltip on hover: Show summary
Log Entries Section:
<div class="entries">
<div class="filters">
<!-- Filter controls: by level, by resource, by time range -->
</div>
<div class="entry" id="entry-{index}">
<div class="entry-header">
<span class="timestamp">{formatted-timestamp}</span>
<span class="level badge-{level}">{level}</span>
<span class="source">{filename}:{line-number}</span>
</div>
<div class="entry-summary">{summary}</div>
<details class="entry-details">
<summary>Show full content</summary>
<pre><code>{content}</code></pre>
</details>
</div>
</div>
CSS Styling:
- Modern, clean design with good contrast
- Responsive layout
- Badge colors: info=gray, warn=yellow, error=red
- Monospace font for log content
- Syntax highlighting for JSON (in audit logs)
JavaScript Interactivity:
// Timeline click handler
document.querySelectorAll('.timeline-event').forEach(el => {
el.addEventListener('click', () => {
const entryId = el.dataset.entryId;
document.getElementById(entryId).scrollIntoView({behavior: 'smooth'});
});
});
// Filter controls
// Expand/collapse details
// Search within entries
Write HTML to file
- Script automatically writes to
.work/prow-job-analyze-resource/{build_id}/{resource_name}.html
- Includes proper HTML5 structure
- All CSS and JavaScript are inline for portability
Step 8: Present Results to User
Display summary
Resource Lifecycle Analysis Complete
Prow Job: {prowjob-name}
Build ID: {build_id}
Target: {target}
Resources Analyzed:
- {resource-spec-1}
- {resource-spec-2}
...
Artifacts downloaded to: .work/prow-job-analyze-resource/{build_id}/logs/
Results:
- Audit log entries: {audit-count}
- Pod log entries: {pod-count}
- Total entries: {total-count}
- Time range: {min_time} to {max_time}
Report generated: .work/prow-job-analyze-resource/{build_id}/{resource_name}.html
Open in browser to view interactive timeline and detailed entries.
Open report in browser
- Detect platform and automatically open the HTML report in the default browser
- Linux:
xdg-open .work/prow-job-analyze-resource/{build_id}/{resource_name}.html
- macOS:
open .work/prow-job-analyze-resource/{build_id}/{resource_name}.html
- Windows:
start .work/prow-job-analyze-resource/{build_id}/{resource_name}.html
- On Linux (most common for this environment), use
xdg-open
Offer next steps
- Ask if user wants to search for additional resources in the same job
- Ask if user wants to analyze a different Prow job
- Explain that artifacts are cached in
.work/prow-job-analyze-resource/{build_id}/ for faster subsequent searches
Error Handling
Handle these error scenarios gracefully:
Invalid URL format
- Error: "URL must contain 'test-platform-results/' substring"
- Provide example of valid URL
Build ID not found
- Error: "Could not find build ID (10+ decimal digits) in URL path"
- Explain requirement and show URL parsing
gcloud not installed
gcloud not authenticated
- Detect with:
gcloud auth list
- Instruct: "Please run: gcloud auth login"
No access to bucket
- Error from gcloud storage commands
- Explain: "You need read access to the test-platform-results GCS bucket"
- Suggest checking project access
prowjob.json not found
- Suggest verifying URL and checking if job completed
- Provide gcsweb URL for manual verification
Not a ci-operator job
- Error: "This is not a ci-operator job. No --target found in prowjob.json."
- Explain: Only ci-operator jobs can be analyzed by this skill
gather-extra not found
- Warn: "gather-extra directory not found for target {target}"
- Suggest: Job may not have completed or target name is incorrect
No matches found
- Display: "No log entries found matching the specified resources"
- Suggest:
- Check resource names for typos
- Try searching without kind or namespace filters
- Verify resources existed during this job execution
Timestamp parsing failures
- Warn about unparseable timestamps
- Fall back to line order for sorting
- Still include entries in report
Performance Considerations
Avoid re-downloading
- Check if
.work/prow-job-analyze-resource/{build_id}/logs/ already has content
- Ask user before re-downloading
Efficient downloads
- Use
gcloud storage cp -r for recursive downloads
- Use
--no-user-output-enabled to suppress verbose output
- Create target directories with
mkdir -p before downloading to avoid gcloud errors
Memory efficiency
- The
parse_all_logs.py script processes log files incrementally (line by line)
- Don't load entire files into memory
- Script outputs to JSON for efficient HTML generation
Content length limits
- The HTML generator trims JSON content to ~2000 chars in display
- Full content is available in expandable details sections
Progress indicators
- Show "Downloading audit logs..." before gcloud commands
- Show "Parsing audit logs..." before running parse script
- Show "Generating HTML report..." before running report generator
Examples
Example 1: Search for a namespace/project
User: "Analyze e2e-test-project-api-p28m in this Prow job: https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-release-master-okd-scos-4.20-e2e-aws-ovn-techpreview/1964725888612306944"
Output:
- Downloads artifacts to: .work/prow-job-analyze-resource/1964725888612306944/logs/
- Finds actual resource name: e2e-test-project-api-p28mx (namespace)
- Parses 382 audit log entries
- Finds 86 pod log mentions
- Creates: .work/prow-job-analyze-resource/1964725888612306944/e2e-test-project-api-p28mx.html
- Shows timeline from creation (18:11:02) to deletion (18:17:32)
Example 2: Search for a pod
User: "Analyze pod/etcd-0 in this Prow job: https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/30393/pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn/1978913325970362368/"
Output:
- Creates: .work/prow-job-analyze-resource/1978913325970362368/etcd-0.html
- Shows timeline of all pod/etcd-0 events across namespaces
Example 3: Search by name only
User: "Find all resources named cluster-version-operator in job {url}"
Output:
- Searches without kind filter
- Finds deployments, pods, services, etc. all named cluster-version-operator
- Creates: .work/prow-job-analyze-resource/{build_id}/cluster-version-operator.html
Example 4: Search for multiple resources using regex
User: "Analyze e2e-test-project-api-pkjxf and e2e-test-project-api-7zdxx in job {url}"
Output:
- Uses regex pattern: `e2e-test-project-api-pkjxf|e2e-test-project-api-7zdxx`
- Finds all events for both namespaces in a single pass
- Parses 1,047 total entries (501 for first namespace, 546 for second)
- Passes the same pattern to generate_html_report.py
- HTML displays: "Resources: e2e-test-project-api-7zdxx, e2e-test-project-api-pkjxf"
- Creates: .work/prow-job-analyze-resource/{build_id}/e2e-test-project-api-pkjxf.html
- Timeline shows interleaved events from both namespaces chronologically
Tips
- Always verify gcloud prerequisites before starting (gcloud CLI must be installed)
- Authentication is NOT required - the bucket is publicly accessible
- Use
.work/prow-job-analyze-resource/{build_id}/ directory structure for organization
- All work files are in
.work/ which is already in .gitignore
- The Python scripts handle all parsing and HTML generation - use them!
- Cache artifacts in
.work/prow-job-analyze-resource/{build_id}/ to speed up subsequent searches
- The parse script supports regex patterns for flexible matching:
- Use
resource1|resource2 to search for multiple resources in a single pass
- Use
.* wildcards to match resource name patterns
- Simple substring matching still works for basic searches
- The resource name provided by the user may not exactly match the actual resource name in logs
- Example: User asks for
e2e-test-project-api-p28m but actual resource is e2e-test-project-api-p28mx
- Use regex patterns like
e2e-test-project-api-p28m.* to find partial matches
- For namespaces/projects, search for the resource name - it will match both
namespace and project resources
- Provide helpful error messages with actionable solutions
Important Notes
Resource Name Matching:
- The parse script uses regex pattern matching for maximum flexibility
- Supports pipe operator (
|) to search for multiple resources: resource1|resource2
- Supports wildcards (
.*) for pattern matching: e2e-test-.*
- Simple substrings still work for basic searches
- May match multiple related resources (e.g., namespace, project, rolebindings in that namespace)
- Report all matches - this provides complete lifecycle context
Namespace vs Project:
- In OpenShift, a
project is essentially a namespace with additional metadata
- Searching for a namespace will find both namespace and project resources
- The audit logs contain events for both resource types
Target Extraction:
- Must extract the
--target argument from prowjob.json
- This is critical for finding the correct gather-extra path
- Non-ci-operator jobs cannot be analyzed (they don't have --target)
Working with Scripts:
- All scripts are in
plugins/prow-job/skills/prow-job-analyze-resource/
parse_all_logs.py - Parses audit logs and pod logs, outputs JSON
- Detects glog severity levels (E=error, W=warn, I=info, F=fatal)
- Supports regex patterns for resource matching
generate_html_report.py - Generates interactive HTML report from JSON
- Scripts output status messages to stderr for progress display. JSON output to stdout is clean.
Pod Log Glog Format Support:
- The parser automatically detects and parses glog format logs
- Glog format:
E0910 11:43:41.153414 ...
E = severity (E/F → error, W → warn, I → info)
0910 = month/day (MMDD)
11:43:41.153414 = time with microseconds
- Timestamp parsing: Extracts timestamp and infers year (2025)
- Severity mapping allows filtering by level in HTML report
- Non-glog logs default to info level
1---2name: prow-job-analyze-resource3description: Analyze Kubernetes resource lifecycle in Prow CI job artifacts by parsing audit logs and pod logs from GCS, generating interactive HTML reports with timelines4---5
6# Prow Job Analyze Resource
7
8This skill analyzes the lifecycle of Kubernetes resources during Prow CI job execution by downloading and parsing artifacts from Google Cloud Storage.
9
10## When to Use This Skill
11
12Use this skill when the user wants to:
13- Debug Prow CI test failures by tracking resource state changes
14- Understand when and how a Kubernetes resource was created, modified, or deleted during a test
15- Analyze resource lifecycle across audit logs and pod logs from ephemeral test clusters
16- Generate interactive HTML reports showing resource events over time
17- Search for specific resources (pods, deployments, configmaps, etc.) in Prow job artifacts
18
19## Prerequisites
20
21Before starting, verify these prerequisites:
22
231. **gcloud CLI Installation**
24 - Check if installed: `which gcloud`
25 - If not installed, provide instructions for the user's platform
26 - Installation guide: https://cloud.google.com/sdk/docs/install
27
282. **gcloud Authentication (Optional)**
29 - The `test-platform-results` bucket is publicly accessible
30 - No authentication is required for read access
31 - Skip authentication checks
32
33## Input Format
34
35The user will provide:
361. **Prow job URL** - gcsweb URL containing `test-platform-results/`
37 - Example: `https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/30393/pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn/1978913325970362368/`
38 - URL may or may not have trailing slash
39
402. **Resource specifications** - Comma-delimited list in format `[namespace:][kind/]name`
41 - Supports regex patterns for matching multiple resources
42 - Examples:
43 - `pod/etcd-0` - pod named etcd-0 in any namespace
44 - `openshift-etcd:pod/etcd-0` - pod in specific namespace
45 - `etcd-0` - any resource named etcd-0 (no kind filter)
46 - `pod/etcd-0,configmap/cluster-config` - multiple resources
47 - `resource-name-1|resource-name-2` - multiple resources using regex OR
48 - `e2e-test-project-api-.*` - all resources matching the pattern
49
50## Implementation Steps
51
52### Step 1: Parse and Validate URL
53
541. **Extract bucket path**
55 - Find `test-platform-results/` in URL
56 - Extract everything after it as the GCS bucket relative path
57 - If not found, error: "URL must contain 'test-platform-results/'"
58
592. **Extract build_id**
60 - Search for pattern `/(\d{10,})/` in the bucket path
61 - build_id must be at least 10 consecutive decimal digits
62 - Handle URLs with or without trailing slash
63 - If not found, error: "Could not find build ID (10+ digits) in URL"
64
653. **Extract prowjob name**
66 - Find the path segment immediately preceding build_id
67 - Example: In `.../pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn/1978913325970362368/`
68 - Prowjob name: `pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn`
69
704. **Construct GCS paths**
71 - Bucket: `test-platform-results`
72 - Base GCS path: `gs://test-platform-results/{bucket-path}/`
73 - Ensure path ends with `/`
74
75### Step 2: Parse Resource Specifications
76
77For each comma-delimited resource spec:
78
791. **Parse format** `[namespace:][kind/]name`
80 - Split on `:` to get namespace (optional)
81 - Split remaining on `/` to get kind (optional) and name (required)
82 - Store as structured data: `{namespace, kind, name}`
83
842. **Validate**
85 - name is required
86 - namespace and kind are optional
87 - Examples:
88 - `pod/etcd-0` → `{kind: "pod", name: "etcd-0"}`
89 - `openshift-etcd:pod/etcd-0` → `{namespace: "openshift-etcd", kind: "pod", name: "etcd-0"}`
90 - `etcd-0` → `{name: "etcd-0"}`
91
92### Step 3: Create Working Directory
93
941. **Check for existing artifacts first**
95 - Check if `.work/prow-job-analyze-resource/{build_id}/logs/` directory exists and has content
96 - If it exists with content:
97 - Use AskUserQuestion tool to ask:
98 - Question: "Artifacts already exist for build {build_id}. Would you like to use the existing download or re-download?"
99 - Options:
100 - "Use existing" - Skip to artifact parsing step (Step 6)
101 - "Re-download" - Continue to clean and re-download
102 - If user chooses "Re-download":
103 - Remove all existing content: `rm -rf .work/prow-job-analyze-resource/{build_id}/logs/`
104 - Also remove tmp directory: `rm -rf .work/prow-job-analyze-resource/{build_id}/tmp/`
105 - This ensures clean state before downloading new content
106 - If user chooses "Use existing":
107 - Skip directly to Step 6 (Parse Audit Logs)
108 - Still need to download prowjob.json if it doesn't exist
109
1102. **Create directory structure**
111 ```bash
112 mkdir -p .work/prow-job-analyze-resource/{build_id}/logs
113 mkdir -p .work/prow-job-analyze-resource/{build_id}/tmp
114 ```
115 - Use `.work/prow-job-analyze-resource/` as the base directory (already in .gitignore)
116 - Use build_id as subdirectory name
117 - Create `logs/` subdirectory for all downloads
118 - Create `tmp/` subdirectory for temporary files (intermediate JSON, etc.)
119 - Working directory: `.work/prow-job-analyze-resource/{build_id}/`
120
121### Step 4: Download and Validate prowjob.json
122
1231. **Download prowjob.json**
124 ```bash
125 gcloud storage cp gs://test-platform-results/{bucket-path}/prowjob.json .work/prow-job-analyze-resource/{build_id}/logs/prowjob.json --no-user-output-enabled
126 ```
127
1282. **Parse and validate**
129 - Read `.work/prow-job-analyze-resource/{build_id}/logs/prowjob.json`
130 - Search for pattern: `--target=([a-zA-Z0-9-]+)`
131 - If not found:
132 - Display: "This is not a ci-operator job. The prowjob cannot be analyzed by this skill."
133 - Explain: ci-operator jobs have a --target argument specifying the test target
134 - Exit skill
135
1363. **Extract target name**
137 - Capture the target value (e.g., `e2e-aws-ovn`)
138 - Store for constructing gather-extra path
139
140### Step 5: Download Audit Logs and Pod Logs
141
1421. **Construct gather-extra paths**
143 - GCS path: `gs://test-platform-results/{bucket-path}/artifacts/{target}/gather-extra/`
144 - Local path: `.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/`
145
1462. **Download audit logs**
147 ```bash
148 mkdir -p .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs
149 gcloud storage cp -r gs://test-platform-results/{bucket-path}/artifacts/{target}/gather-extra/artifacts/audit_logs/ .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs/ --no-user-output-enabled
150 ```
151 - Create directory first to avoid gcloud errors
152 - Use `--no-user-output-enabled` to suppress progress output
153 - If directory not found, warn: "No audit logs found. Job may not have completed or audit logging may be disabled."
154
1553. **Download pod logs**
156 ```bash
157 mkdir -p .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods
158 gcloud storage cp -r gs://test-platform-results/{bucket-path}/artifacts/{target}/gather-extra/artifacts/pods/ .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods/ --no-user-output-enabled
159 ```
160 - Create directory first to avoid gcloud errors
161 - Use `--no-user-output-enabled` to suppress progress output
162 - If directory not found, warn: "No pod logs found."
163
164### Step 6: Parse Audit Logs and Pod Logs
165
166**IMPORTANT: Use the provided Python script `parse_all_logs.py` from the skill directory to parse both audit logs and pod logs efficiently.**
167
168**Usage:**
169```bash
170python3 plugins/prow-job/skills/prow-job-analyze-resource/parse_all_logs.py <resource_pattern> \
171 .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs \
172 .work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods \
173 > .work/prow-job-analyze-resource/{build_id}/tmp/all_entries.json
174```
175
176**Resource Pattern Parameter:**
177- The `<resource_pattern>` parameter supports **regex patterns**
178- Use `|` (pipe) to search for multiple resources: `resource1|resource2|resource3`
179- Use `.*` for wildcards: `e2e-test-project-.*`
180- Simple substring matching still works: `my-namespace`
181- Examples:
182 - Single resource: `e2e-test-project-api-pkjxf`
183 - Multiple resources: `e2e-test-project-api-pkjxf|e2e-test-project-api-7zdxx`
184 - Pattern matching: `e2e-test-project-api-.*`
185
186**Note:** The script outputs status messages to stderr which will display as progress. The JSON output to stdout is clean and ready to use.
187
188**What the script does:**
189
1901. **Find all log files**
191 - Audit logs: `.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/audit_logs/**/*.log`
192 - Pod logs: `.work/prow-job-analyze-resource/{build_id}/logs/artifacts/{target}/gather-extra/artifacts/pods/**/*.log`
193
1942. **Parse audit log files (JSONL format)**
195 - Read file line by line
196 - Each line is a JSON object (JSONL format)
197 - Parse JSON into object `e`
198
1993. **Extract fields from each audit log entry**
200 - `e.verb` - action (get, list, create, update, patch, delete, watch)
201 - `e.user.username` - user making request
202 - `e.responseStatus.code` - HTTP response code (integer)
203 - `e.objectRef.namespace` - namespace (if namespaced)
204 - `e.objectRef.resource` - lowercase plural kind (e.g., "pods", "configmaps")
205 - `e.objectRef.name` - resource name
206 - `e.requestReceivedTimestamp` - ISO 8601 timestamp
207
2084. **Filter matches for each resource spec**
209 - Uses **regex matching** on `e.objectRef.namespace` and `e.objectRef.name`
210 - Pattern matches if found in either namespace or name field
211 - Supports all regex features:
212 - Pipe operator: `resource1|resource2` matches either resource
213 - Wildcards: `e2e-test-.*` matches all resources starting with `e2e-test-`
214 - Character classes: `[abc]` matches a, b, or c
215 - Simple substring matching still works for patterns without regex special chars
216 - Performance optimization: plain strings use fast substring search
217
2185. **For each audit log match, capture**
219 - **Source**: "audit"
220 - **Filename**: Full path to .log file
221 - **Line number**: Line number in file (1-indexed)
222 - **Level**: Based on `e.responseStatus.code`
223 - 200-299: "info"
224 - 400-499: "warn"
225 - 500-599: "error"
226 - **Timestamp**: Parse `e.requestReceivedTimestamp` to datetime
227 - **Content**: Full JSON line (for expandable details)
228 - **Summary**: Generate formatted summary
229 - Format: `{verb} {resource}/{name} in {namespace} by {username} → HTTP {code}`
230 - Example: `create pod/etcd-0 in openshift-etcd by system:serviceaccount:kube-system:deployment-controller → HTTP 201`
231
2326. **Parse pod log files (plain text format)**
233 - Read file line by line
234 - Each line is plain text (not JSON)
235 - Search for resource pattern in line content
236
2377. **For each pod log match, capture**
238 - **Source**: "pod"
239 - **Filename**: Full path to .log file
240 - **Line number**: Line number in file (1-indexed)
241 - **Level**: Detect from glog format or default to "info"
242 - Glog format: `E0910 11:43:41.153414 ...` (E=error, W=warn, I=info, F=fatal→error)
243 - Non-glog format: default to "info"
244 - **Timestamp**: Extract from start of line if present (format: `YYYY-MM-DDTHH:MM:SS.mmmmmmZ`)
245 - **Content**: Full log line
246 - **Summary**: First 200 characters of line (after timestamp if present)
247
2488. **Combine and sort all entries**
249 - Merge audit log entries and pod log entries
250 - Sort all entries chronologically by timestamp
251 - Entries without timestamps are placed at the end
252
253### Step 7: Generate HTML Report
254
255**IMPORTANT: Use the provided Python script `generate_html_report.py` from the skill directory.**
256
257**Usage:**
258```bash
259python3 plugins/prow-job/skills/prow-job-analyze-resource/generate_html_report.py \
260 .work/prow-job-analyze-resource/{build_id}/tmp/all_entries.json \
261 "{prowjob_name}" \
262 "{build_id}" \
263 "{target}" \
264 "{resource_pattern}" \
265 "{gcsweb_url}"
266```
267
268**Resource Pattern Parameter:**
269- The `{resource_pattern}` should be the **same pattern used in the parse script**
270- For single resources: `e2e-test-project-api-pkjxf`
271- For multiple resources: `e2e-test-project-api-pkjxf|e2e-test-project-api-7zdxx`
272- The script will parse the pattern to display the searched resources in the HTML header
273
274**Output:** The script generates `.work/prow-job-analyze-resource/{build_id}/{first_resource_name}.html`
275
276**What the script does:**
277
2781. **Determine report filename**
279 - Format: `.work/prow-job-analyze-resource/{build_id}/{resource_name}.html`
280 - Uses the primary resource name for the filename
281
2822. **Sort all entries by timestamp**
283 - Loads audit log entries from JSON
284 - Sort chronologically (ascending)
285 - Entries without timestamps go at the end
286
2873. **Calculate timeline bounds**
288 - min_time: Earliest timestamp found
289 - max_time: Latest timestamp found
290 - Time range: max_time - min_time
291
2924. **Generate HTML structure**
293
294 **Header Section:**
295 ```html
296 <div class="header">
297 <h1>Prow Job Resource Lifecycle Analysis</h1>
298 <div class="metadata">
299 <p><strong>Prow Job:</strong> {prowjob-name}</p>
300 <p><strong>Build ID:</strong> {build_id}</p>
301 <p><strong>gcsweb URL:</strong> <a href="{original-url}">{original-url}</a></p>
302 <p><strong>Target:</strong> {target}</p>
303 <p><strong>Resources:</strong> {resource-list}</p>
304 <p><strong>Total Entries:</strong> {count}</p>
305 <p><strong>Time Range:</strong> {min_time} to {max_time}</p>
306 </div>
307 </div>
308 ```
309
310 **Interactive Timeline:**
311 ```html
312 <div class="timeline-container">
313 <svg id="timeline" width="100%" height="100">
314 <!-- For each entry, render colored vertical line -->
315 <line x1="{position}%" y1="0" x2="{position}%" y2="100"
316 stroke="{color}" stroke-width="2"
317 class="timeline-event" data-entry-id="{entry-id}"
318 title="{summary}">
319 </line>
320 </svg>
321 </div>
322 ```
323 - Position: Calculate percentage based on timestamp between min_time and max_time
324 - Color: white/lightgray (info), yellow (warn), red (error)
325 - Clickable: Jump to corresponding entry
326 - Tooltip on hover: Show summary
327
328 **Log Entries Section:**
329 ```html
330 <div class="entries">
331 <div class="filters">
332 <!-- Filter controls: by level, by resource, by time range -->
333 </div>
334
335 <div class="entry" id="entry-{index}">
336 <div class="entry-header">
337 <span class="timestamp">{formatted-timestamp}</span>
338 <span class="level badge-{level}">{level}</span>
339 <span class="source">{filename}:{line-number}</span>
340 </div>
341 <div class="entry-summary">{summary}</div>
342 <details class="entry-details">
343 <summary>Show full content</summary>
344 <pre><code>{content}</code></pre>
345 </details>
346 </div>
347 </div>
348 ```
349
350 **CSS Styling:**
351 - Modern, clean design with good contrast
352 - Responsive layout
353 - Badge colors: info=gray, warn=yellow, error=red
354 - Monospace font for log content
355 - Syntax highlighting for JSON (in audit logs)
356
357 **JavaScript Interactivity:**
358 ```javascript
359 // Timeline click handler
360 document.querySelectorAll('.timeline-event').forEach(el => {
361 el.addEventListener('click', () => {
362 const entryId = el.dataset.entryId;
363 document.getElementById(entryId).scrollIntoView({behavior: 'smooth'});
364 });
365 });
366
367 // Filter controls
368 // Expand/collapse details
369 // Search within entries
370 ```
371
3725. **Write HTML to file**
373 - Script automatically writes to `.work/prow-job-analyze-resource/{build_id}/{resource_name}.html`
374 - Includes proper HTML5 structure
375 - All CSS and JavaScript are inline for portability
376
377### Step 8: Present Results to User
378
3791. **Display summary**
380 ```
381 Resource Lifecycle Analysis Complete
382
383 Prow Job: {prowjob-name}
384 Build ID: {build_id}
385 Target: {target}
386
387 Resources Analyzed:
388 - {resource-spec-1}
389 - {resource-spec-2}
390 ...
391
392 Artifacts downloaded to: .work/prow-job-analyze-resource/{build_id}/logs/
393
394 Results:
395 - Audit log entries: {audit-count}
396 - Pod log entries: {pod-count}
397 - Total entries: {total-count}
398 - Time range: {min_time} to {max_time}
399
400 Report generated: .work/prow-job-analyze-resource/{build_id}/{resource_name}.html
401
402 Open in browser to view interactive timeline and detailed entries.
403 ```
404
4052. **Open report in browser**
406 - Detect platform and automatically open the HTML report in the default browser
407 - Linux: `xdg-open .work/prow-job-analyze-resource/{build_id}/{resource_name}.html`
408 - macOS: `open .work/prow-job-analyze-resource/{build_id}/{resource_name}.html`
409 - Windows: `start .work/prow-job-analyze-resource/{build_id}/{resource_name}.html`
410 - On Linux (most common for this environment), use `xdg-open`
411
4123. **Offer next steps**
413 - Ask if user wants to search for additional resources in the same job
414 - Ask if user wants to analyze a different Prow job
415 - Explain that artifacts are cached in `.work/prow-job-analyze-resource/{build_id}/` for faster subsequent searches
416
417## Error Handling
418
419Handle these error scenarios gracefully:
420
4211. **Invalid URL format**
422 - Error: "URL must contain 'test-platform-results/' substring"
423 - Provide example of valid URL
424
4252. **Build ID not found**
426 - Error: "Could not find build ID (10+ decimal digits) in URL path"
427 - Explain requirement and show URL parsing
428
4293. **gcloud not installed**
430 - Detect with: `which gcloud`
431 - Provide installation instructions for user's platform
432 - Link: https://cloud.google.com/sdk/docs/install
433
4344. **gcloud not authenticated**
435 - Detect with: `gcloud auth list`
436 - Instruct: "Please run: gcloud auth login"
437
4385. **No access to bucket**
439 - Error from gcloud storage commands
440 - Explain: "You need read access to the test-platform-results GCS bucket"
441 - Suggest checking project access
442
4436. **prowjob.json not found**
444 - Suggest verifying URL and checking if job completed
445 - Provide gcsweb URL for manual verification
446
4477. **Not a ci-operator job**
448 - Error: "This is not a ci-operator job. No --target found in prowjob.json."
449 - Explain: Only ci-operator jobs can be analyzed by this skill
450
4518. **gather-extra not found**
452 - Warn: "gather-extra directory not found for target {target}"
453 - Suggest: Job may not have completed or target name is incorrect
454
4559. **No matches found**
456 - Display: "No log entries found matching the specified resources"
457 - Suggest:
458 - Check resource names for typos
459 - Try searching without kind or namespace filters
460 - Verify resources existed during this job execution
461
46210. **Timestamp parsing failures**
463 - Warn about unparseable timestamps
464 - Fall back to line order for sorting
465 - Still include entries in report
466
467## Performance Considerations
468
4691. **Avoid re-downloading**
470 - Check if `.work/prow-job-analyze-resource/{build_id}/logs/` already has content
471 - Ask user before re-downloading
472
4732. **Efficient downloads**
474 - Use `gcloud storage cp -r` for recursive downloads
475 - Use `--no-user-output-enabled` to suppress verbose output
476 - Create target directories with `mkdir -p` before downloading to avoid gcloud errors
477
4783. **Memory efficiency**
479 - The `parse_all_logs.py` script processes log files incrementally (line by line)
480 - Don't load entire files into memory
481 - Script outputs to JSON for efficient HTML generation
482
4834. **Content length limits**
484 - The HTML generator trims JSON content to ~2000 chars in display
485 - Full content is available in expandable details sections
486
4875. **Progress indicators**
488 - Show "Downloading audit logs..." before gcloud commands
489 - Show "Parsing audit logs..." before running parse script
490 - Show "Generating HTML report..." before running report generator
491
492## Examples
493
494### Example 1: Search for a namespace/project
495```
496User: "Analyze e2e-test-project-api-p28m in this Prow job: https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/periodic-ci-openshift-release-master-okd-scos-4.20-e2e-aws-ovn-techpreview/1964725888612306944"
497
498Output:
499- Downloads artifacts to: .work/prow-job-analyze-resource/1964725888612306944/logs/
500- Finds actual resource name: e2e-test-project-api-p28mx (namespace)
501- Parses 382 audit log entries
502- Finds 86 pod log mentions
503- Creates: .work/prow-job-analyze-resource/1964725888612306944/e2e-test-project-api-p28mx.html
504- Shows timeline from creation (18:11:02) to deletion (18:17:32)
505```
506
507### Example 2: Search for a pod
508```
509User: "Analyze pod/etcd-0 in this Prow job: https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/30393/pull-ci-openshift-origin-main-okd-scos-e2e-aws-ovn/1978913325970362368/"
510
511Output:
512- Creates: .work/prow-job-analyze-resource/1978913325970362368/etcd-0.html
513- Shows timeline of all pod/etcd-0 events across namespaces
514```
515
516### Example 3: Search by name only
517```
518User: "Find all resources named cluster-version-operator in job {url}"
519
520Output:
521- Searches without kind filter
522- Finds deployments, pods, services, etc. all named cluster-version-operator
523- Creates: .work/prow-job-analyze-resource/{build_id}/cluster-version-operator.html
524```
525
526### Example 4: Search for multiple resources using regex
527```
528User: "Analyze e2e-test-project-api-pkjxf and e2e-test-project-api-7zdxx in job {url}"
529
530Output:
531- Uses regex pattern: `e2e-test-project-api-pkjxf|e2e-test-project-api-7zdxx`
532- Finds all events for both namespaces in a single pass
533- Parses 1,047 total entries (501 for first namespace, 546 for second)
534- Passes the same pattern to generate_html_report.py
535- HTML displays: "Resources: e2e-test-project-api-7zdxx, e2e-test-project-api-pkjxf"
536- Creates: .work/prow-job-analyze-resource/{build_id}/e2e-test-project-api-pkjxf.html
537- Timeline shows interleaved events from both namespaces chronologically
538```
539
540## Tips
541
542- Always verify gcloud prerequisites before starting (gcloud CLI must be installed)
543- Authentication is NOT required - the bucket is publicly accessible
544- Use `.work/prow-job-analyze-resource/{build_id}/` directory structure for organization
545- All work files are in `.work/` which is already in .gitignore
546- The Python scripts handle all parsing and HTML generation - use them!
547- Cache artifacts in `.work/prow-job-analyze-resource/{build_id}/` to speed up subsequent searches
548- The parse script supports **regex patterns** for flexible matching:
549 - Use `resource1|resource2` to search for multiple resources in a single pass
550 - Use `.*` wildcards to match resource name patterns
551 - Simple substring matching still works for basic searches
552- The resource name provided by the user may not exactly match the actual resource name in logs
553 - Example: User asks for `e2e-test-project-api-p28m` but actual resource is `e2e-test-project-api-p28mx`
554 - Use regex patterns like `e2e-test-project-api-p28m.*` to find partial matches
555- For namespaces/projects, search for the resource name - it will match both `namespace` and `project` resources
556- Provide helpful error messages with actionable solutions
557
558## Important Notes
559
5601. **Resource Name Matching:**
561 - The parse script uses **regex pattern matching** for maximum flexibility
562 - Supports pipe operator (`|`) to search for multiple resources: `resource1|resource2`
563 - Supports wildcards (`.*`) for pattern matching: `e2e-test-.*`
564 - Simple substrings still work for basic searches
565 - May match multiple related resources (e.g., namespace, project, rolebindings in that namespace)
566 - Report all matches - this provides complete lifecycle context
567
5682. **Namespace vs Project:**
569 - In OpenShift, a `project` is essentially a `namespace` with additional metadata
570 - Searching for a namespace will find both namespace and project resources
571 - The audit logs contain events for both resource types
572
5733. **Target Extraction:**
574 - Must extract the `--target` argument from prowjob.json
575 - This is critical for finding the correct gather-extra path
576 - Non-ci-operator jobs cannot be analyzed (they don't have --target)
577
5784. **Working with Scripts:**
579 - All scripts are in `plugins/prow-job/skills/prow-job-analyze-resource/`
580 - `parse_all_logs.py` - Parses audit logs and pod logs, outputs JSON
581 - Detects glog severity levels (E=error, W=warn, I=info, F=fatal)
582 - Supports regex patterns for resource matching
583 - `generate_html_report.py` - Generates interactive HTML report from JSON
584 - Scripts output status messages to stderr for progress display. JSON output to stdout is clean.
585
5865. **Pod Log Glog Format Support:**
587 - The parser automatically detects and parses glog format logs
588 - Glog format: `E0910 11:43:41.153414 ...`
589 - `E` = severity (E/F → error, W → warn, I → info)
590 - `0910` = month/day (MMDD)
591 - `11:43:41.153414` = time with microseconds
592 - Timestamp parsing: Extracts timestamp and infers year (2025)
593 - Severity mapping allows filtering by level in HTML report
594 - Non-glog logs default to info level