agones.dev/agones@v1.53.0/build/report/report.go (about)

     1  // Copyright 2023 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //	http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // build/report/report.go generates a flake report for the last N weeks
    16  // on the configured build trigger. It creates files in `tmp/report`, a
    17  // dated YYYY-MM-DD.html report and an index.html with a redirect to the
    18  // new date, intended to upload to Google Cloud Storage.
    19  package main
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"html/template"
    25  	"log"
    26  	"os"
    27  	"sort"
    28  	"time"
    29  
    30  	cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2"
    31  	cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb"
    32  	"google.golang.org/api/iterator"
    33  )
    34  
    35  const (
    36  	window             = time.Hour * 24 * 7 * 4 // 4 weeks
    37  	wantBuildTriggerID = "da003bb8-e9bb-4983-a556-e77fb92f17ca"
    38  	outPath            = "tmp/report"
    39  
    40  	reportTemplate = `
    41  <!DOCTYPE html>
    42  <html lang="en">
    43  <head>
    44      <meta charset="UTF-8">
    45      <title>Flake Report From {{ .WindowStart }} to {{ .WindowEnd }}</title>
    46      <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
    47  </head>
    48  <body>
    49  	<header>
    50  	Flake Report {{ .WindowStart }} to {{ .WindowEnd }}
    51  	</header>
    52  	<p><b>Flake Ratio:</b> {{ printf "%.3f" .FlakeRatio }} ({{ .FlakeCount }} flakes / {{ .BuildCount }} successful builds)</p>
    53  
    54  	<table>
    55  		<tr>
    56  			<th>Time</th>
    57  			<th>Flaky Build</th>
    58  		</tr>
    59  {{- range .Flakes -}}
    60  		<tr>
    61  			<td>{{ .CreateTime }}</td>
    62  			<td><a href="https://console.cloud.google.com/cloud-build/builds;region=global/{{ .ID }}?project=agones-images">{{ .ID }}</a></td>
    63  		</tr>
    64  {{- end -}}
    65  	</table>
    66  
    67  	<p><b>Methodology:</b> For every successful build of a given commit hash, we count the number of failed
    68  	builds on the same commit hash. The <em>Flake Ratio</em> is the ratio of flakes to successes, giving an
    69  	expected value for how many times a build has to be retried before succeeding, on the same SHA.
    70  	This methodology only covers manual re-runs in Cloud Build - builds retried via rebase are not counted,
    71  	as in general it's difficult to attribute flakes between commit hashes.	
    72  </body>
    73  </html>
    74  `
    75  
    76  	redirectTemplate = `
    77  <html xmlns="http://www.w3.org/1999/xhtml">
    78    <head>
    79      <title>Latest build</title>
    80      <meta http-equiv="refresh" content="0;URL='https://agones-build-reports.storage.googleapis.com/{{ .Date }}.html'" />
    81    </head>
    82    <body>
    83      <p><a href="https://agones-build-reports.storage.googleapis.com/{{ .Date }}.html">Latest build report (redirecting now).</a></p>
    84    </body>
    85  </html>  
    86  `
    87  )
    88  
    89  type report struct {
    90  	WindowStart string
    91  	WindowEnd   string
    92  	Flakes      []flake
    93  	FlakeCount  int
    94  	BuildCount  int
    95  	FlakeRatio  float32
    96  }
    97  
    98  type flake struct {
    99  	ID         string
   100  	CreateTime string
   101  }
   102  
   103  type redirect struct {
   104  	Date string
   105  }
   106  
   107  func main() {
   108  	ctx := context.Background()
   109  	reportTmpl := newReportTemplate()
   110  	redirTmpl := newRedirectTemplate()
   111  
   112  	windowEnd := time.Now().UTC()
   113  	windowStart := windowEnd.Add(-window)
   114  	date := windowEnd.Format("2006-01-02")
   115  
   116  	err := os.MkdirAll(outPath, 0o755)
   117  	if err != nil {
   118  		log.Fatalf("failed to create output path %v: %v", outPath, err)
   119  	}
   120  
   121  	datePath := fmt.Sprintf("%s/%s.html", outPath, date)
   122  	reportFile, err := os.OpenFile(datePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
   123  	if err != nil {
   124  		log.Fatalf("failed to open report %v: %v", datePath, err)
   125  	}
   126  
   127  	redirPath := fmt.Sprintf("%s/index.html", outPath)
   128  	redirFile, err := os.OpenFile(redirPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
   129  	if err != nil {
   130  		log.Fatalf("failed to open redirect %v: %v", redirPath, err)
   131  	}
   132  
   133  	c, err := cloudbuild.NewClient(ctx)
   134  	if err != nil {
   135  		log.Fatalf("failed to initialize cloudbuild client: %v", err)
   136  	}
   137  
   138  	success := make(map[string]bool)     // build SHA -> bool
   139  	failure := make(map[string][]string) // build SHA -> slice of build IDs that failed
   140  	idTime := make(map[string]time.Time) // build ID -> create time
   141  
   142  	// See https://pkg.go.dev/cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb#ListBuildsRequest.
   143  	req := &cloudbuildpb.ListBuildsRequest{
   144  		ProjectId: "agones-images",
   145  		// TODO(zmerlynn): No idea why this is failing.
   146  		// Filter:    `build_trigger_id = "da003bb8-e9bb-4983-a556-e77fb92f17ca"`,
   147  	}
   148  	it := c.ListBuilds(ctx, req)
   149  	for {
   150  		resp, err := it.Next()
   151  		if err == iterator.Done {
   152  			break
   153  		}
   154  		if err != nil {
   155  			log.Fatalf("error listing builds: %v", err)
   156  		}
   157  		createTime := resp.CreateTime.AsTime()
   158  		if createTime.Before(windowStart) {
   159  			break
   160  		}
   161  		// We only care about Agones builds.
   162  		if resp.BuildTriggerId != wantBuildTriggerID {
   163  			continue
   164  		}
   165  		// Ignore if it's still running.
   166  		if resp.FinishTime == nil {
   167  			continue
   168  		}
   169  
   170  		id := resp.Id
   171  		sha := resp.Substitutions["COMMIT_SHA"]
   172  		status := resp.Status
   173  		idTime[id] = createTime
   174  		log.Printf("id = %v, sha = %v, status = %v", id, sha, status)
   175  
   176  		// Record clear cut success/failure, not timeout, cancelled, etc.
   177  		switch status {
   178  		case cloudbuildpb.Build_SUCCESS:
   179  			success[sha] = true
   180  		case cloudbuildpb.Build_FAILURE:
   181  			failure[sha] = append(failure[sha], id)
   182  		default:
   183  			continue
   184  		}
   185  	}
   186  
   187  	buildCount := len(success)
   188  	flakeCount := 0
   189  	var flakes []flake
   190  	for sha := range success {
   191  		flakeCount += len(failure[sha])
   192  		for _, id := range failure[sha] {
   193  			flakes = append(flakes, flake{
   194  				ID:         id,
   195  				CreateTime: idTime[id].Format(time.RFC3339),
   196  			})
   197  		}
   198  	}
   199  	sort.Slice(flakes, func(i, j int) bool { return flakes[i].CreateTime > flakes[j].CreateTime })
   200  
   201  	if err := reportTmpl.Execute(reportFile, report{
   202  		WindowStart: windowStart.Format("2006-01-02"),
   203  		WindowEnd:   windowEnd.Format("2006-01-02"),
   204  		BuildCount:  buildCount,
   205  		FlakeCount:  flakeCount,
   206  		FlakeRatio:  float32(flakeCount) / float32(buildCount),
   207  		Flakes:      flakes,
   208  	}); err != nil {
   209  		log.Fatalf("failure rendering report: %v", err)
   210  	}
   211  
   212  	if err := redirTmpl.Execute(redirFile, redirect{Date: date}); err != nil {
   213  		log.Fatalf("failure rendering redirect: %v", err)
   214  	}
   215  }
   216  
   217  func newReportTemplate() *template.Template {
   218  	return template.Must(template.New("report").Parse(reportTemplate)).Option("missingkey=error")
   219  }
   220  
   221  func newRedirectTemplate() *template.Template {
   222  	return template.Must(template.New("redir").Parse(redirectTemplate)).Option("missingkey=error")
   223  }