Skip to content

Commit 9ee9387

Browse files
authored
[Monitoring] Adding a build age metric (#4341)
### Motivation We currently lack awareness on how old builds are during fuzz task. This PR implements that, by making the assumption that the Last Update Time metadata field in GCS is a good proxy for build age. [Documentation reference](https://cloud.google.com/storage/docs/json_api/v1/objects#resource) ### Approach Symbolized and custom builds do not matter, thus all builds of interest will be fetched from ```build_manager.setup_regular_build```. Logic for collecting all bucket paths and the latest revision was refactored, so that ```setup_regular_build``` can also figure out the latest revision for a given build and conditionally emit the proposed metric. ### Testing strategy !Todo: test this for fuzz, analyze, progression Locally ran tasks, with instructions from #4343 and #4345 , and verified the _emmit_build_age_metric function gets invoked and produces sane output. Commands used: ``` fuzz libFuzzer libfuzzer_asan_log4j2 ``` ![image](https://github.com/user-attachments/assets/66937297-20ec-44cf-925e-0004a905c92e) ``` progression 4992158360403968 libfuzzer_asan_qt ``` ![image](https://github.com/user-attachments/assets/0e1f1199-d1d8-4da5-814e-8d8409d1f806) ``` analyze 4992158360403968 libfuzzer_asan_qt (disclaimer: build revision was overriden mid flight to force a trunk build, since this testcase was already tied to a crash revision) ``` ![image](https://github.com/user-attachments/assets/dd3d5a60-36a1-4a9e-a21b-b72177ffdecd) Part of #4271
1 parent ba9009a commit 9ee9387

File tree

2 files changed

+81
-23
lines changed

2 files changed

+81
-23
lines changed

src/clusterfuzz/_internal/build_management/build_manager.py

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from collections import namedtuple
1717
import contextlib
18+
import datetime
1819
import os
1920
import re
2021
import shutil
@@ -1199,8 +1200,65 @@ def _get_latest_revision(bucket_paths):
11991200
return None
12001201

12011202

1202-
def setup_trunk_build(bucket_paths, fuzz_target, build_prefix=None):
1203+
def _emit_build_age_metric(gcs_path):
1204+
"""Emits a metric to track the age of a build."""
1205+
try:
1206+
last_update_time = storage.get(gcs_path).get('updated')
1207+
# TODO(vitorguidi): standardize return type between fs and gcs.
1208+
if isinstance(last_update_time, str):
1209+
# storage.get returns two different types for the updated field:
1210+
# the gcs api returns string, and the local filesystem implementation
1211+
# returns a datetime.datetime object normalized for UTC.
1212+
last_update_time = datetime.datetime.fromisoformat(last_update_time)
1213+
now = datetime.datetime.now(datetime.timezone.utc)
1214+
elapsed_time = now - last_update_time
1215+
elapsed_time_in_hours = elapsed_time.total_seconds() / 3600
1216+
# Fuzz targets do not apply for custom builds
1217+
labels = {
1218+
'job': os.getenv('JOB_NAME'),
1219+
'platform': environment.platform(),
1220+
'task': os.getenv('TASK_NAME'),
1221+
}
1222+
monitoring_metrics.JOB_BUILD_AGE.add(elapsed_time_in_hours, labels)
1223+
# This field is expected as a datetime object
1224+
# https://cloud.google.com/storage/docs/json_api/v1/objects#resource
1225+
except Exception as e:
1226+
logs.error(f'Failed to emit build age metric for {gcs_path}: {e}')
1227+
1228+
1229+
def _get_build_url(bucket_path: Optional[str], revision: int,
1230+
job_type: Optional[str]):
1231+
"""Returns the GCS url for a build, given a bucket path and revision"""
1232+
build_urls = get_build_urls_list(bucket_path)
1233+
if not build_urls:
1234+
logs.error('Error getting build urls for job %s.' % job_type)
1235+
return None
1236+
build_url = revisions.find_build_url(bucket_path, build_urls, revision)
1237+
if not build_url:
1238+
logs.error(
1239+
'Error getting build url for job %s (r%d).' % (job_type, revision))
1240+
return None
1241+
return build_url
1242+
1243+
1244+
def _get_build_bucket_paths():
1245+
"""Returns gcs bucket endpoints that contain the build of interest."""
1246+
bucket_paths = []
1247+
for env_var in DEFAULT_BUILD_BUCKET_PATH_ENV_VARS:
1248+
bucket_path = get_bucket_path(env_var)
1249+
if bucket_path:
1250+
bucket_paths.append(bucket_path)
1251+
else:
1252+
logs.info('Bucket path not found for %s' % env_var)
1253+
return bucket_paths
1254+
1255+
1256+
def setup_trunk_build(fuzz_target, build_prefix=None):
12031257
"""Sets up latest trunk build."""
1258+
bucket_paths = _get_build_bucket_paths()
1259+
if not bucket_paths:
1260+
logs.error('Attempted a trunk build, but no bucket paths were found.')
1261+
return None
12041262
latest_revision = _get_latest_revision(bucket_paths)
12051263
if latest_revision is None:
12061264
logs.error('Unable to find a matching revision.')
@@ -1221,24 +1279,24 @@ def setup_trunk_build(bucket_paths, fuzz_target, build_prefix=None):
12211279
def setup_regular_build(revision,
12221280
bucket_path=None,
12231281
build_prefix='',
1224-
fuzz_target=None) -> RegularBuild:
1282+
fuzz_target=None) -> Optional[RegularBuild]:
12251283
"""Sets up build with a particular revision."""
12261284
if not bucket_path:
12271285
# Bucket path can be customized, otherwise get it from the default env var.
12281286
bucket_path = get_bucket_path('RELEASE_BUILD_BUCKET_PATH')
12291287

1230-
build_urls = get_build_urls_list(bucket_path)
12311288
job_type = environment.get_value('JOB_NAME')
1232-
if not build_urls:
1233-
logs.error('Error getting build urls for job %s.' % job_type)
1234-
return None
1235-
build_url = revisions.find_build_url(bucket_path, build_urls, revision)
1236-
if not build_url:
1237-
logs.error(
1238-
'Error getting build url for job %s (r%d).' % (job_type, revision))
1289+
build_url = _get_build_url(bucket_path, revision, job_type)
12391290

1291+
if not build_url:
12401292
return None
12411293

1294+
all_bucket_paths = _get_build_bucket_paths()
1295+
latest_revision = _get_latest_revision(all_bucket_paths)
1296+
1297+
if revision == latest_revision:
1298+
_emit_build_age_metric(build_url)
1299+
12421300
# build_url points to a GCP bucket, and we're only converting it to its HTTP
12431301
# endpoint so that we can use remote unzipping.
12441302
http_build_url = build_url.replace('gs://', 'https://storage.googleapis.com/')
@@ -1377,19 +1435,7 @@ def _setup_build(revision, fuzz_target):
13771435
return setup_regular_build(revision, fuzz_target=fuzz_target)
13781436

13791437
# If no revision is provided, we default to a trunk build.
1380-
bucket_paths = []
1381-
for env_var in DEFAULT_BUILD_BUCKET_PATH_ENV_VARS:
1382-
bucket_path = get_bucket_path(env_var)
1383-
if bucket_path:
1384-
bucket_paths.append(bucket_path)
1385-
else:
1386-
logs.info('Bucket path not found for %s' % env_var)
1387-
1388-
if len(bucket_paths) == 0:
1389-
logs.error('Attempted a trunk build, but no bucket paths were found.')
1390-
return None
1391-
1392-
return setup_trunk_build(bucket_paths, fuzz_target=fuzz_target)
1438+
return setup_trunk_build(fuzz_target=fuzz_target)
13931439

13941440

13951441
def is_custom_binary():

src/clusterfuzz/_internal/metrics/monitoring_metrics.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232
monitor.BooleanField('bad_build'),
3333
])
3434

35+
JOB_BUILD_AGE = monitor.CumulativeDistributionMetric(
36+
'job/build_age',
37+
bucketer=monitor.FixedWidthBucketer(width=0.05, num_finite_buckets=20),
38+
description=('Distribution of latest build\'s age in hours. '
39+
'(grouped by fuzzer/job)'),
40+
field_spec=[
41+
monitor.StringField('job'),
42+
monitor.StringField('platform'),
43+
monitor.StringField('task'),
44+
],
45+
)
46+
3547
JOB_BUILD_RETRIEVAL_TIME = monitor.CumulativeDistributionMetric(
3648
'task/build_retrieval_time',
3749
bucketer=monitor.FixedWidthBucketer(width=0.05, num_finite_buckets=20),

0 commit comments

Comments
 (0)