Skip to content

Commit ad15896

Browse files
committed
dashboard: export bug archives as .jsonl.gz to GCS
To facilitate syzbot statistics analysis, export jsonl files with per-bug info to GCS for selected namespaces.
1 parent 75b437f commit ad15896

File tree

4 files changed

+128
-0
lines changed

4 files changed

+128
-0
lines changed

dashboard/app/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ type Config struct {
139139
Coverage *CoverageConfig
140140
// Reproducers export path.
141141
ReproExportPath string
142+
// Bug archive export path (a .jsonl.gz file).
143+
// Should be of the form bucket/full_path.
144+
BugArchiveExportPath string
142145
}
143146

144147
// ACLItem is an Access Control List item.

dashboard/app/cron.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ cron:
3939
# Weekly update the kernel-file -> subsystems relationship
4040
- url: /cron/update_coverdb_subsystems
4141
schedule: every monday
42+
- url: /cron/export_bugs
43+
schedule: every day 01:00

dashboard/app/export.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package main
5+
6+
import (
7+
"compress/gzip"
8+
"context"
9+
"encoding/json"
10+
"errors"
11+
"fmt"
12+
"net/http"
13+
14+
"github.com/google/syzkaller/dashboard/api"
15+
"github.com/google/syzkaller/pkg/gcs"
16+
"golang.org/x/sync/errgroup"
17+
"google.golang.org/appengine/v2"
18+
db "google.golang.org/appengine/v2/datastore"
19+
"google.golang.org/appengine/v2/log"
20+
)
21+
22+
func handleExportBugs(w http.ResponseWriter, r *http.Request) {
23+
c := appengine.NewContext(r)
24+
for ns, nsConfig := range getConfig(c).Namespaces {
25+
if nsConfig.BugArchiveExportPath == "" {
26+
continue
27+
}
28+
log.Infof(c, "exporting bugs for %q", ns)
29+
err := uploadBugsJSONL(c, ns, nsConfig.BugArchiveExportPath)
30+
if err != nil {
31+
log.Errorf(c, "failed to export %q bugs: %v", ns, err)
32+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
33+
return
34+
}
35+
}
36+
}
37+
38+
const (
39+
// With 16 threads, it takes 6-7 minutes to export 26k bugs
40+
// from the upstream namespace. 32 threads do not accelerate it.
41+
exportQueryThreads = 32
42+
exportAccessLevel = AccessPublic
43+
)
44+
45+
func uploadBugsJSONL(c context.Context, ns, path string) error {
46+
stream := make(chan *api.Bug, exportQueryThreads)
47+
eg, ctx := errgroup.WithContext(c)
48+
eg.Go(func() error {
49+
defer close(stream)
50+
return queryBugInfos(ctx, ns, stream)
51+
})
52+
eg.Go(func() error {
53+
return uploadJSONL(ctx, path, stream)
54+
})
55+
return eg.Wait()
56+
}
57+
58+
func queryBugInfos(c context.Context, ns string, stream chan<- *api.Bug) error {
59+
bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query {
60+
return query.Filter("Namespace=", ns)
61+
})
62+
if err != nil {
63+
return fmt.Errorf("failed to load bugs: %w", err)
64+
}
65+
log.Infof(c, "loaded %d bugs", len(bugs))
66+
eg, ctx := errgroup.WithContext(c)
67+
eg.SetLimit(exportQueryThreads)
68+
for _, bug := range bugs {
69+
if exportAccessLevel < bug.sanitizeAccess(ctx, exportAccessLevel) {
70+
continue
71+
}
72+
eg.Go(func() error {
73+
details, err := loadBugDetails(ctx, bug, exportAccessLevel)
74+
if err != nil {
75+
return err
76+
}
77+
select {
78+
case stream <- getExtAPIDescrForBug(details):
79+
return nil
80+
case <-ctx.Done():
81+
return ctx.Err()
82+
}
83+
})
84+
}
85+
return eg.Wait()
86+
}
87+
88+
func uploadJSONL(c context.Context, path string, stream <-chan *api.Bug) error {
89+
client, err := gcs.NewClient(c)
90+
if err != nil {
91+
return fmt.Errorf("failed create a gcs client: %w", err)
92+
}
93+
wc, err := client.FileWriter(path, "application/gzip", "")
94+
if err != nil {
95+
return fmt.Errorf("file writer ext failed: %w", err)
96+
}
97+
gzWriter := gzip.NewWriter(wc)
98+
enc := json.NewEncoder(gzWriter)
99+
count := 0
100+
for bug := range stream {
101+
err := enc.Encode(bug)
102+
if err != nil {
103+
return fmt.Errorf("failed to encode bug ID=%s: %w", bug.ID, err)
104+
}
105+
count++
106+
}
107+
log.Infof(c, "%d bugs exported", count)
108+
// Save the file only if the context has not been canceled by now.
109+
if ctxErr := c.Err(); ctxErr != nil {
110+
if errors.Is(ctxErr, context.Canceled) {
111+
return nil
112+
}
113+
return ctxErr
114+
}
115+
if err := gzWriter.Close(); err != nil {
116+
return fmt.Errorf("failed to close gzip writer: %w", err)
117+
}
118+
if err := wc.Close(); err != nil {
119+
return fmt.Errorf("unable to close writer: %w", err)
120+
}
121+
return client.Publish(path)
122+
}

dashboard/app/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func initHTTPHandlers() {
8989
http.HandleFunc("/cron/refresh_subsystems", handleRefreshSubsystems)
9090
http.HandleFunc("/cron/subsystem_reports", handleSubsystemReports)
9191
http.HandleFunc("/cron/update_coverdb_subsystems", handleUpdateCoverDBSubsystems)
92+
http.HandleFunc("/cron/export_bugs", handleExportBugs)
9293
}
9394

9495
func handleMovedPermanently(dest string) http.HandlerFunc {

0 commit comments

Comments
 (0)