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)