github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/testing/analyzers/github_issues_utils.py (about) 1 # 2 # Licensed to the Apache Software Foundation (ASF) under one or more 3 # contributor license agreements. See the NOTICE file distributed with 4 # this work for additional information regarding copyright ownership. 5 # The ASF licenses this file to You under the Apache License, Version 2.0 6 # (the "License"); you may not use this file except in compliance with 7 # the License. You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 # 17 import json 18 import logging 19 import os 20 from typing import List 21 from typing import Optional 22 from typing import Tuple 23 24 import pandas as pd 25 import requests 26 27 try: 28 _GITHUB_TOKEN: Optional[str] = os.environ['GITHUB_TOKEN'] 29 except KeyError as e: 30 _GITHUB_TOKEN = None 31 logging.warning( 32 'A Github Personal Access token is required ' 33 'to create Github Issues.') 34 35 _BEAM_GITHUB_REPO_OWNER = 'apache' 36 _BEAM_GITHUB_REPO_NAME = 'beam' 37 # Adding GitHub Rest API version to the header to maintain version stability. 38 # For more information, please look at 39 # https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/ # pylint: disable=line-too-long 40 _HEADERS = { 41 "Authorization": 'token {}'.format(_GITHUB_TOKEN), 42 "Accept": "application/vnd.github+json", 43 "X-GitHub-Api-Version": "2022-11-28" 44 } 45 46 _ISSUE_TITLE_TEMPLATE = """ 47 Performance Regression or Improvement: {}:{} 48 """ 49 50 _ISSUE_DESCRIPTION_TEMPLATE = """ 51 Performance change found in the 52 test: `{}` for the metric: `{}`. 53 54 For more information on how to triage the alerts, please look at 55 `Triage performance alert issues` section of the [README](https://github.com/apache/beam/tree/master/sdks/python/apache_beam/testing/analyzers/README.md#triage-performance-alert-issues). 56 """ 57 _METRIC_INFO_TEMPLATE = "timestamp: {}, metric_value: `{}`" 58 _AWAITING_TRIAGE_LABEL = 'awaiting triage' 59 _PERF_ALERT_LABEL = 'perf-alert' 60 61 62 def create_issue( 63 title: str, 64 description: str, 65 labels: Optional[List[str]] = None, 66 ) -> Tuple[int, str]: 67 """ 68 Create an issue with title, description with a label. 69 70 Args: 71 title: GitHub issue title. 72 description: GitHub issue description. 73 labels: Labels used to tag the GitHub issue. 74 Returns: 75 Tuple containing GitHub issue number and issue URL. 76 """ 77 url = "https://api.github.com/repos/{}/{}/issues".format( 78 _BEAM_GITHUB_REPO_OWNER, _BEAM_GITHUB_REPO_NAME) 79 data = { 80 'owner': _BEAM_GITHUB_REPO_OWNER, 81 'repo': _BEAM_GITHUB_REPO_NAME, 82 'title': title, 83 'body': description, 84 'labels': [_AWAITING_TRIAGE_LABEL, _PERF_ALERT_LABEL] 85 } 86 if labels: 87 data['labels'].extend(labels) # type: ignore 88 response = requests.post( 89 url=url, data=json.dumps(data), headers=_HEADERS).json() 90 return response['number'], response['html_url'] 91 92 93 def comment_on_issue(issue_number: int, 94 comment_description: str) -> Tuple[bool, str]: 95 """ 96 This method looks for an issue with provided issue_number. If an open 97 issue is found, comment on the open issue with provided description else 98 do nothing. 99 100 Args: 101 issue_number: A GitHub issue number. 102 comment_description: If an issue with issue_number is open, 103 then comment on the issue with the using comment_description. 104 Returns: 105 tuple[bool, Optional[str]] indicating if a comment was added to 106 issue, and the comment URL. 107 """ 108 url = 'https://api.github.com/repos/{}/{}/issues/{}'.format( 109 _BEAM_GITHUB_REPO_OWNER, _BEAM_GITHUB_REPO_NAME, issue_number) 110 open_issue_response = requests.get( 111 url, 112 json.dumps({ 113 'owner': _BEAM_GITHUB_REPO_OWNER, 114 'repo': _BEAM_GITHUB_REPO_NAME, 115 'issue_number': issue_number 116 }, 117 default=str), 118 headers=_HEADERS).json() 119 if open_issue_response['state'] == 'open': 120 data = { 121 'owner': _BEAM_GITHUB_REPO_OWNER, 122 'repo': _BEAM_GITHUB_REPO_NAME, 123 'body': comment_description, 124 issue_number: issue_number, 125 } 126 response = requests.post( 127 open_issue_response['comments_url'], json.dumps(data), headers=_HEADERS) 128 return True, response.json()['html_url'] 129 return False, '' 130 131 132 def add_awaiting_triage_label(issue_number: int): 133 url = 'https://api.github.com/repos/{}/{}/issues/{}/labels'.format( 134 _BEAM_GITHUB_REPO_OWNER, _BEAM_GITHUB_REPO_NAME, issue_number) 135 requests.post( 136 url, json.dumps({'labels': [_AWAITING_TRIAGE_LABEL]}), headers=_HEADERS) 137 138 139 def get_issue_description( 140 test_name: str, 141 metric_name: str, 142 timestamps: List[pd.Timestamp], 143 metric_values: List, 144 change_point_index: int, 145 max_results_to_display: int = 5) -> str: 146 """ 147 Args: 148 metric_name: Metric name used for the Change Point Analysis. 149 timestamps: Timestamps of the metrics when they were published to the 150 Database. Timestamps are expected in ascending order. 151 metric_values: metric values for the previous runs. 152 change_point_index: Index for the change point. The element in the 153 index of the metric_values would be the change point. 154 max_results_to_display: Max number of results to display from the change 155 point index, in both directions of the change point index. 156 157 Returns: 158 str: Description used to fill the GitHub issues description. 159 """ 160 161 # TODO: Add mean and median before and after the changepoint index. 162 max_timestamp_index = min( 163 change_point_index + max_results_to_display, len(metric_values) - 1) 164 min_timestamp_index = max(0, change_point_index - max_results_to_display) 165 166 description = _ISSUE_DESCRIPTION_TEMPLATE.format( 167 test_name, metric_name) + 2 * '\n' 168 169 runs_to_display = [ 170 _METRIC_INFO_TEMPLATE.format(timestamps[i].ctime(), metric_values[i]) 171 for i in reversed(range(min_timestamp_index, max_timestamp_index + 1)) 172 ] 173 174 runs_to_display[change_point_index - min_timestamp_index] += " <---- Anomaly" 175 return description + '\n'.join(runs_to_display) 176 177 178 def report_change_point_on_issues( 179 title: str, 180 description: str, 181 labels: Optional[List[str]] = None, 182 existing_issue_number: Optional[int] = None, 183 ) -> Tuple[int, str]: 184 """ 185 Comments the description on the existing issue (if provided and still open), 186 or creates a new issue. 187 """ 188 if existing_issue_number is not None: 189 commented_on_issue, issue_url = comment_on_issue( 190 issue_number=existing_issue_number, 191 comment_description=description 192 ) 193 if commented_on_issue: 194 add_awaiting_triage_label(issue_number=existing_issue_number) 195 return existing_issue_number, issue_url 196 return create_issue(title=title, description=description, labels=labels)