cmd/tc-collector/artifactDownloader.go (132 lines of code) (raw):

package main import ( "compress/gzip" "context" "errors" "fmt" "io" "net/http" "net/url" "path" "strconv" "strings" "time" "github.com/JetBrains/ij-perf-report-aggregator/pkg/tc-properties" "github.com/cenkalti/backoff/v4" ) type ArtifactItem struct { data []byte path string } func (t *Collector) downloadReports(ctx context.Context, build Build) ([]ArtifactItem, error) { var result []ArtifactItem err := t.findAndDownloadStartUpReports(ctx, build, build.Artifacts.File, &result) if err != nil { return nil, err } return result, nil } func (t *Collector) findAndDownloadStartUpReports(ctx context.Context, build Build, artifacts []Artifact, result *[]ArtifactItem) error { for _, artifact := range artifacts { name := path.Base(artifact.Url) if strings.HasSuffix(artifact.Url, ".json") && strings.HasPrefix(name, "startup-stats") || strings.HasSuffix(name, ".performance.json") || strings.HasSuffix(artifact.Url, ".json") && (strings.Contains(artifact.Url, "metrics") && name != "action.invoked.json" && name != "spans.json" && name != "fleet_backend.json") || strings.HasSuffix(name, "-ijperf.json") || t.config.DbName == "jbr" && strings.HasSuffix(name, ".txt") || t.config.DbName == "qodana" && (name == "open-telemetry.json" || name == "metrics.json") { artifactUrlString := t.serverUrl + strings.Replace(strings.TrimPrefix(artifact.Url, "/app/rest"), "/artifacts/metadata/", "/artifacts/content/", 1) report, err := t.downloadStartUpReportWithRetries(ctx, build, artifactUrlString) if err != nil { t.logger.Error("Failed to download artifact, skipping", "buildTypeId", build.Type, "buildId", build.Id, "artifact", artifactUrlString, "error", err) continue } *result = append(*result, ArtifactItem{ data: report, path: artifactUrlString, }) continue } err := t.findAndDownloadStartUpReports(ctx, build, artifact.Children.File, result) if err != nil { return err } } return nil } func (t *Collector) downloadStartUpReport(ctx context.Context, build Build, artifactUrlString string) ([]byte, error) { artifactUrl, err := url.Parse(artifactUrlString) if err != nil { return nil, fmt.Errorf("failed to parse artifact url: %w", err) } response, err := t.get(ctx, artifactUrl.String()) if err != nil { t.logger.Error("Download failed", "error", err) return nil, err } defer response.Body.Close() if response.StatusCode > 300 { if response.StatusCode == http.StatusNotFound && build.Status == "FAILURE" { t.logger.Warn("no report", "id", build.Id, "status", build.Status) return nil, nil } responseBody, _ := io.ReadAll(response.Body) t.logger.Warn("invalid response on downloading artifacts, skipping", "status", response.Status, "body", responseBody) return nil, nil } t.storeSessionIdCookie(response) // ReadAll is used because report not only required to be decoded, but also stored as is (after minification) data, err := io.ReadAll(response.Body) if err != nil { t.logger.Error("Failed to read response body", "error", err) return nil, err } return data, nil } func (t *Collector) downloadStartUpReportWithRetries(ctx context.Context, build Build, artifactUrlString string) ([]byte, error) { bo := backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(15*time.Second), backoff.WithMaxInterval(5*time.Second)) var result []byte err := backoff.Retry(func() error { if err := ctx.Err(); err != nil { return backoff.Permanent(fmt.Errorf("context cancelled or deadline exceeded: %w", err)) } data, err := t.downloadStartUpReport(ctx, build, artifactUrlString) if err != nil || data == nil { return fmt.Errorf("download failed: %w", err) } result = data return nil }, bo) if err != nil { return nil, errors.New("maximum retries reached, download failed") } return result, nil } func (t *Collector) downloadBuildProperties(ctx context.Context, build Build) ([]byte, error) { artifactUrl, err := url.Parse(t.serverUrl + "/builds/id:" + strconv.Itoa(build.Id) + "/artifacts/content/.teamcity/properties/build.start.properties.gz") if err != nil { return nil, err } response, err := t.get(ctx, artifactUrl.String()) if err != nil { return nil, err } defer response.Body.Close() if response.StatusCode > 300 { if response.StatusCode == http.StatusNotFound { t.logger.Warn("build.start.properties not found", "url", artifactUrl.String()) return nil, nil } responseBody, _ := io.ReadAll(response.Body) return nil, fmt.Errorf("invalid response (%s): %s", response.Status, responseBody) } t.storeSessionIdCookie(response) gzipReader, err := gzip.NewReader(response.Body) if err != nil { return nil, err } data, err := io.ReadAll(gzipReader) if err != nil { return nil, err } return tc_properties.ReadProperties(data) }