github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/benchmark/internal/cireport/imagereport.go (about) 1 package cireport 2 3 import ( 4 "context" 5 "fmt" 6 "path" 7 "regexp" 8 "strconv" 9 "strings" 10 "text/template" 11 "time" 12 "unicode" 13 14 "github.com/sirupsen/logrus" 15 "golang.org/x/sync/errgroup" 16 "golang.org/x/text/transform" 17 "golang.org/x/text/unicode/norm" 18 19 "github.com/pyroscope-io/pyroscope/benchmark/internal/config" 20 ) 21 22 type Uploader interface { 23 WriteFile(dest string, data []byte) (string, error) 24 } 25 26 type DashboardScreenshotter interface { 27 AllPanels(ctx context.Context, dashboardUID string, from int64, to int64) ([]Panel, error) 28 } 29 30 type ImageReporter struct { 31 uploader Uploader 32 screenshotter DashboardScreenshotter 33 } 34 35 func ImageReportCLI(cfg config.ImageReport) (string, error) { 36 uploader, err := decideUploader(cfg.UploadType, cfg.UploadBucket) 37 if err != nil { 38 return "", err 39 } 40 41 gs := GrafanaScreenshotter{ 42 GrafanaURL: cfg.GrafanaAddress, 43 TimeoutSeconds: cfg.TimeoutSeconds, 44 } 45 46 r := NewImageReporter(gs, uploader) 47 48 from, to := decideTimestamp(cfg.From, cfg.To) 49 50 return r.Report( 51 context.Background(), 52 cfg.DashboardUID, 53 cfg.UploadDest, 54 from, 55 to, 56 ) 57 } 58 59 type screenshotPanel struct { 60 Title string 61 URL string 62 } 63 64 func NewImageReporter(screenshotter GrafanaScreenshotter, uploader Uploader) *ImageReporter { 65 return &ImageReporter{ 66 uploader, 67 &screenshotter, 68 } 69 } 70 71 func (r *ImageReporter) Report(ctx context.Context, dashboardUID string, dir string, from int64, to int64) (string, error) { 72 // screenshot all panes 73 logrus.Debug("taking screenshot of all panels") 74 panels, err := r.screenshotter.AllPanels(ctx, dashboardUID, from, to) 75 if err != nil { 76 return "", err 77 } 78 79 sp := make([]screenshotPanel, len(panels)) 80 81 // upload 82 logrus.Debug("uploading screenshots") 83 g, ctx := errgroup.WithContext(ctx) 84 for i, p := range panels { 85 p := p 86 i := i 87 88 g.Go(func() error { 89 publicURL, err := r.uploader.WriteFile(filename(dir, p.Title), p.Data) 90 if err != nil { 91 return err 92 } 93 94 // TODO lock this? 95 sp[i].Title = p.Title 96 sp[i].URL = publicURL 97 return nil 98 }) 99 } 100 101 if err := g.Wait(); err != nil { 102 return "", err 103 } 104 105 logrus.Debug("generating markdown report") 106 return r.tpl(sp) 107 } 108 109 func filename(dir string, s string) string { 110 return path.Join(dir, normalizeWord(s)+".png") 111 } 112 113 func normalizeWord(s string) string { 114 isMn := func(r rune) bool { 115 return unicode.Is(unicode.Mn, r) 116 } 117 118 // unicode -> ascii 119 t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) 120 // TODO: handle error 121 result, _, _ := transform.String(t, s) 122 123 result = strings.ReplaceAll(result, " ", "_") 124 // TODO: handle error 125 reg, _ := regexp.Compile("[^a-zA-Z0-9_]+") 126 result = reg.ReplaceAllString(result, "") 127 128 result = strings.ToLower(result) 129 130 return result 131 } 132 133 func (*ImageReporter) tpl(panels []screenshotPanel) (string, error) { 134 var tpl strings.Builder 135 136 data := struct { 137 Panels []screenshotPanel 138 Ts string 139 }{ 140 Panels: panels, 141 // cache bust 142 Ts: strconv.FormatInt(time.Now().Unix(), 10), 143 } 144 t, err := template.New("image-report.gotpl"). 145 Funcs(template.FuncMap{}). 146 ParseFS(resources, "resources/image-report.gotpl") 147 if err != nil { 148 return "", err 149 } 150 151 if err := t.Execute(&tpl, data); err != nil { 152 return "", err 153 } 154 155 return tpl.String(), nil 156 } 157 158 func decideTimestamp(fromInt, toInt int) (from, to int64) { 159 now := time.Now() 160 from = int64(fromInt) 161 to = int64(toInt) 162 163 // set defaults if appropriate 164 if to == 0 { 165 // TODO use UnixMilli() 166 to = now.UnixNano() / int64(time.Millisecond) 167 } 168 169 if from == 0 { 170 // TODO use UnixMilli() 171 from = now.Add(time.Duration(5)*-time.Minute).UnixNano() / int64(time.Millisecond) 172 } 173 174 return from, to 175 } 176 177 func decideUploader(uploadType string, uploadBucket string) (Uploader, error) { 178 var uploader Uploader 179 switch uploadType { 180 case "s3": 181 u, err := NewS3Writer(uploadBucket) 182 uploader = u 183 184 if err != nil { 185 return nil, err 186 } 187 case "fs": 188 uploader = &FsWriter{} 189 default: 190 return nil, fmt.Errorf("invalid upload type: '%s'", uploadType) 191 } 192 193 return uploader, nil 194 }