jenkins_pipeline_metrics.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import os
  9. import json
  10. import requests
  11. import traceback
  12. import csv
  13. import sys
  14. from datetime import datetime, timezone
  15. from requests.auth import HTTPBasicAuth
  16. cur_dir = os.path.dirname(os.path.abspath(__file__))
  17. sys.path.insert(0, os.path.abspath(f'{cur_dir}/../../../util'))
  18. import util
  19. class JenkinsAPIClient:
  20. def __init__(self, jenkins_base_url, jenkins_username, jenkins_api_token):
  21. self.jenkins_base_url = jenkins_base_url.rstrip('/')
  22. self.jenkins_username = jenkins_username
  23. self.jenkins_api_token = jenkins_api_token
  24. self.blueocean_api_path = '/blue/rest/organizations/jenkins/pipelines'
  25. def get_request(self, url, retry=1):
  26. for i in range(retry):
  27. try:
  28. response = requests.get(url, auth=HTTPBasicAuth(self.jenkins_username, self.jenkins_api_token))
  29. if response.ok:
  30. return response.json()
  31. except Exception:
  32. traceback.print_exc()
  33. print(f'WARN: Get request {url} failed, retying....')
  34. util.error(f'Get request {url} failed, see exception for more details.')
  35. def get_builds(self, pipeline_name, branch_name=''):
  36. url = self.jenkins_base_url + self.blueocean_api_path + f'/{pipeline_name}/{branch_name}/runs'
  37. return self.get_request(url, retry=3)
  38. def get_stages(self, build_number, pipeline_name, branch_name=''):
  39. url = self.jenkins_base_url + self.blueocean_api_path + f'/{pipeline_name}/{branch_name}/runs/{build_number}/nodes'
  40. return self.get_request(url, retry=3)
  41. def generate_build_metrics_csv(env, target_date):
  42. output = []
  43. jenkins_client = JenkinsAPIClient(env['JENKINS_URL'], env['JENKINS_USERNAME'], env['JENKINS_API_TOKEN'])
  44. pipeline_name = env['PIPELINE_NAME']
  45. builds = jenkins_client.get_builds(pipeline_name)
  46. days_to_collect = env['DAYS_TO_COLLECT']
  47. for build in builds:
  48. build_time = datetime.strptime(build['startTime'], '%Y-%m-%dT%H:%M:%S.%f%z')
  49. # Convert startTime to local timezone to compare, because Jenkins server may use a different timezone.
  50. build_date = build_time.astimezone().date()
  51. date_diff = target_date - build_date
  52. # Only collect build result of past {days_to_collect} days.
  53. if date_diff.days > int(days_to_collect):
  54. break
  55. stages = jenkins_client.get_stages(build['id'], pipeline_name)
  56. stage_dict = {}
  57. parallel_stages = []
  58. # Build stage_dict and find all parallel stages
  59. for stage in stages:
  60. stage_dict[stage['id']] = stage
  61. if stage['type'] == 'PARALLEL':
  62. parallel_stages.append(stage)
  63. # Calculate build metrics grouped by parallel stage
  64. def stage_duration_sum(stage):
  65. duration_sum = stage['durationInMillis']
  66. for edge in stage['edges']:
  67. downstream_stage = stage_dict[edge['id']]
  68. duration_sum += stage_duration_sum(downstream_stage)
  69. return duration_sum
  70. for parallel_stage in parallel_stages:
  71. try:
  72. build_info = {
  73. 'job_name': parallel_stage['displayName'],
  74. 'view_name': env['BRANCH_NAME']
  75. }
  76. # UTC datetime is required by BI team
  77. build_info['build_time'] = datetime.fromtimestamp(build_time.timestamp(), timezone.utc)
  78. build_info['build_number'] = build['id']
  79. build_info['job_duration'] = stage_duration_sum(parallel_stage) / 1000 / 60
  80. build_info['status'] = parallel_stage['result']
  81. build_info['clean_build'] = 'True'
  82. print(build_info)
  83. output.append(build_info)
  84. except Exception:
  85. traceback.print_exc()
  86. if output:
  87. with open('spectra_build_metrics.csv', 'w', newline='') as csvfile:
  88. fieldnames = list(output[0].keys())
  89. writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
  90. writer.writeheader()
  91. writer.writerows(output)
  92. def generate_build_metrics_manifest(csv_s3_location):
  93. data = {
  94. "entries": [
  95. {
  96. "url": csv_s3_location
  97. }
  98. ]
  99. }
  100. with open('spectra_build_metrics.manifest', 'w') as manifest:
  101. json.dump(data, manifest)
  102. def upload_files_to_s3(env, formatted_date):
  103. csv_s3_prefix = f"{env['CSV_PREFIX'].rstrip('/')}/{formatted_date}"
  104. manifest_s3_prefix = f"{env['MANIFEST_PREFIX'].rstrip('/')}/{formatted_date}"
  105. upload_to_s3_script_path = os.path.join(cur_dir, 'upload_to_s3.py')
  106. engine_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
  107. if sys.platform == 'win32':
  108. python = os.path.join(engine_root, 'python', 'python.cmd')
  109. else:
  110. python = os.path.join(engine_root, 'python', 'python.sh')
  111. upload_csv_cmd = [python, upload_to_s3_script_path, '--base_dir', cur_dir, '--file_regex', env['CSV_REGEX'], '--bucket', env['BUCKET'], '--key_prefix', csv_s3_prefix]
  112. util.execute_system_call(upload_csv_cmd)
  113. upload_manifest_cmd = [python, upload_to_s3_script_path, '--base_dir', cur_dir, '--file_regex', env['MANIFEST_REGEX'], '--bucket', env['BUCKET'], '--key_prefix', manifest_s3_prefix]
  114. util.execute_system_call(upload_manifest_cmd)
  115. def get_required_env(env, keys):
  116. success = True
  117. for key in keys:
  118. try:
  119. env[key] = os.environ[key].strip()
  120. except KeyError:
  121. util.error(f'{key} is not set in environment variable')
  122. success = False
  123. return success
  124. def main():
  125. env = {}
  126. required_env_list = ['JENKINS_URL', 'PIPELINE_NAME', 'BRANCH_NAME', 'JENKINS_USERNAME', 'JENKINS_API_TOKEN', 'BUCKET', 'CSV_REGEX', 'CSV_PREFIX', 'MANIFEST_REGEX', 'MANIFEST_PREFIX', 'DAYS_TO_COLLECT']
  127. if not get_required_env(env, required_env_list):
  128. util.error('Required environment variable is not set, see log for more details.')
  129. target_date = datetime.today().date()
  130. formatted_date = f'{target_date.year}/{target_date:%m}/{target_date:%d}'
  131. csv_s3_location = f"s3://{env['BUCKET']}/{env['CSV_PREFIX'].rstrip('/')}/{formatted_date}/spectra_build_metrics.csv"
  132. generate_build_metrics_csv(env, target_date)
  133. generate_build_metrics_manifest(csv_s3_location)
  134. upload_files_to_s3(env, formatted_date)
  135. if __name__ == "__main__":
  136. main()