Skip to content

Add a minimal PoC of kcidb-triage #638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
97 changes: 97 additions & 0 deletions kcidb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
"""Kernel CI reporting"""

import datetime
import os
import sys
from io import BytesIO
import gzip
import email
import logging
import requests
from kcidb.misc import LIGHT_ASSERTS
# Silence flake8 "imported but unused" warning
from kcidb import io, db, mq, orm, oo, monitor, tests, unittest, misc # noqa
Expand Down Expand Up @@ -407,3 +412,95 @@ def ingest_main():
)
sys.stdout.write("\x00")
sys.stdout.flush()


def triage_main():
"""Execute the kcidb-triage command-line tool"""
sys.excepthook = misc.log_and_print_excepthook
description = 'kcidb-triage - Triage builds/tests against issues in ' \
'the specified object set'
parser = oo.ArgumentParser(database="json", description=description)
parser.add_argument(
'-b', '--cache-bucket',
help='Name of the cache bucket to try fetching artifacts from. '
'The value of KCIDB_CACHE_BUCKET_NAME is used, if not specified. '
'Otherwise artifacts are downloaded from the original URLs.'
)
args = parser.parse_args()
# Get the objects to be triaged
pattern_set = set()
for pattern_string in args.pattern_strings:
pattern_set |= orm.query.Pattern.parse(pattern_string)
oo_client = oo.Client(db.Client(args.database))
objects = oo_client.query(pattern_set)
# Create the cache client
cache_bucket = args.cache_bucket or \
os.environ.get("KCIDB_CACHE_BUCKET_NAME")
cache_client = cache_bucket and cache.Client(cache_bucket, 0, 0)
cache_ttl = datetime.timedelta(seconds=30)
requests_session = requests.Session()
output = io.SCHEMA.new()
output_incidents = output.setdefault("incidents", [])

def fetch_log(obj):
"""Fetch the text of the main log for an object"""
if not obj.log_url:
return None
log_url = (
cache_client and
cache_client.map(obj.log_url, cache_ttl)
) or obj.log_url
try:
LOGGER.debug("Downloading log for %s ID %r from %r",
type(obj).__name__, obj.get_id(), log_url)
response = requests_session.get(log_url,
timeout=30,
allow_redirects=True)
Comment on lines +456 to +458

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I strongly recommend limiting the download size from the start.
It can avoid unexpected memory usage problems, e.g. CKI has recently submitted a build with a log of 320MB that has caused some problems.

Suggested change
response = requests_session.get(log_url,
timeout=30,
allow_redirects=True)
response = requests_session.get(log_url,
timeout=30,
stream=True,
allow_redirects=True)
content_iterator = response.iter_content(DOWNLOAD_MAX_SIZE)
if not (raw_data := next(content_iterator, None)):
return None

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course. We have that implemented in the artifact archive. I could just copy it from there. This is just a PoC.

except requests.exceptions.RequestException as err:
LOGGER.error("Failed downloading log for %s ID %r "
"from %r: %s", type(obj).__name__,
obj.get_id(), log_url, str(err))
return None
content_type = response.headers['Content-Type']
if content_type == 'application/gzip':
with gzip.GzipFile(fileobj=BytesIO(response.content)) as f:
return f.read().decode('utf-8')
else:
return response.text

# For each issue
for issue in objects["issue"]:
issue_version = issue.latest_version
# For each build
for build in objects.get("build", []):
LOGGER.debug("Triaging build ID %r", build.id)
# If we have the main log for the build
log = fetch_log(build)
# If we detect the issue in the log
# TODO Fetch the actual pattern from the issue
if "undefined reference" in log:
output_incidents.append(dict(
id=f"_:{issue_version.id}:{issue_version.version_num}:"
f"build:{build.id}:error",
Comment on lines +483 to +484

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use parentheses when using implicit concatenation in a parameter list or tuple

Suggested change
id=f"_:{issue_version.id}:{issue_version.version_num}:"
f"build:{build.id}:error",
id=(f"_:{issue_version.id}:{issue_version.version_num}:"
f"build:{build.id}:error"),

There are more instances of this in the changes

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uuuh, OK. I didn't really have that convention so far, but could do.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, this convention avoids skipping commas accidentally, and TBH I find it personally easier to read

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I got the idea. Thanks :)

issue_id=issue.id,
issue_version=issue.version_num,
build_id=build.id,
present=True,
))
# For each test
for test in objects.get("test", []):
LOGGER.debug("Triaging test ID %r", test.id)
# If we have the main log for the test
log = fetch_log(test)
# If we detect the issue in the log
# TODO Fetch the actual pattern from the issue
if "FAIL" in log:
output_incidents.append(dict(
id=f"_:{issue_version.id}:{issue_version.version_num}:"
f"test:{test.id}:error",
issue_id=issue.id,
issue_version=issue.version_num,
test_id=test.id,
present=True,
))
misc.json_dump(output_incidents, sys.stdout, indent=4)
3 changes: 0 additions & 3 deletions kcidb/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2332,7 +2332,6 @@ def test_upgrade(clean_database):
"id":
"_:kernelci:"
"5acb9c2a7bc836e9e5172bbcd2311499c5b4e5f1",
"log_excerpt": None,
"log_url": None,
"message_id": None,
"misc": None,
Expand All @@ -2355,7 +2354,6 @@ def test_upgrade(clean_database):
"duration": None,
"id": "google:google.org:a1d993c3n4c448b2j0l1hbf1",
"input_files": None,
"log_excerpt": None,
"log_url": None,
"misc": None,
"origin": "google",
Expand All @@ -2377,7 +2375,6 @@ def test_upgrade(clean_database):
"environment_misc": {"foo": "bar"},
"id":
"google:google.org:a19di3j5h67f8d9475f26v11",
"log_excerpt": None,
"log_url": None,
"misc": None,
"origin": "google",
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"kcidb-query = kcidb:query_main",
"kcidb-notify = kcidb:notify_main",
"kcidb-ingest = kcidb:ingest_main",
"kcidb-triage = kcidb:triage_main",
"kcidb-db-schemas = kcidb.db:schemas_main",
"kcidb-db-init = kcidb.db:init_main",
"kcidb-db-upgrade = kcidb.db:upgrade_main",
Expand Down
Loading