gopls/internal/telemetry/cmd/stacks: use authentication token
GitHub imposes a stringent rate limit for unauthenticated requests,
that our current rate of telemetry often exceeds.
This change causes the stacks command to read a GitHub
authentication token from $HOME/.stacks.token and use it if found,
relaxing the rate limit. Instructions for creating a token are
recorded in comments.
Fixes golang/go#68733
Change-Id: Ia4b73faa1340dfbed4b9b350d2c57f09abf8ca38
Reviewed-on: https://go-review.googlesource.com/c/tools/+/603155
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go
index fb30c81..3da90f8 100644
--- a/gopls/internal/telemetry/cmd/stacks/stacks.go
+++ b/gopls/internal/telemetry/cmd/stacks/stacks.go
@@ -17,6 +17,8 @@
"log"
"net/http"
"net/url"
+ "os"
+ "path/filepath"
"sort"
"strings"
"time"
@@ -31,6 +33,8 @@
// flags
var (
daysFlag = flag.Int("days", 7, "number of previous days of telemetry data to read")
+
+ token string // optional GitHub authentication token, to relax the rate limit
)
func main() {
@@ -38,6 +42,33 @@
log.SetPrefix("stacks: ")
flag.Parse()
+ // Read GitHub authentication token from $HOME/.stacks.token.
+ //
+ // You can create one using the flow at: GitHub > You > Settings >
+ // Developer Settings > Personal Access Tokens > Fine-grained tokens >
+ // Generate New Token. Generate the token on behalf of yourself
+ // (not "golang" or "google"), with no special permissions.
+ // The token is typically of the form "github_pat_XXX", with 82 hex digits.
+ // Save it in the file, with mode 0400.
+ //
+ // For security, secret tokens should be read from files, not
+ // command-line flags or environment variables.
+ {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ log.Fatal(err)
+ }
+ tokenFile := filepath.Join(home, ".stacks.token")
+ content, err := os.ReadFile(tokenFile)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ log.Fatalf("cannot read GitHub authentication token: %v", err)
+ }
+ log.Printf("no file %s containing GitHub authentication token; continuing without authentication, which is subject to stricter rate limits (https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api).", tokenFile)
+ }
+ token = string(bytes.TrimSpace(content))
+ }
+
// Maps stack text to Version/GoVersion/GOOS/GOARCH string to counter.
stacks := make(map[string]map[string]int64)
var distinctStacks int
@@ -129,10 +160,10 @@
batch := stackIDs[:min(6, len(stackIDs))]
stackIDs = stackIDs[len(batch):]
- query := "label:gopls/telemetry-wins in:body " + strings.Join(batch, " OR ")
+ query := "is:issue label:gopls/telemetry-wins in:body " + strings.Join(batch, " OR ")
res, err := searchIssues(query)
if err != nil {
- log.Fatalf("GitHub issues query failed: %v", err)
+ log.Fatalf("GitHub issues query %q failed: %v", query, err)
}
for _, issue := range res.Items {
for _, id := range batch {
@@ -283,13 +314,22 @@
// searchIssues queries the GitHub issue tracker.
func searchIssues(query string) (*IssuesSearchResult, error) {
q := url.QueryEscape(query)
- resp, err := http.Get(IssuesURL + "?q=" + q)
+
+ req, err := http.NewRequest("GET", IssuesURL+"?q="+q, nil)
+ if err != nil {
+ return nil, err
+ }
+ if token != "" {
+ req.Header.Add("Authorization", "Bearer "+token)
+ }
+ resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
- return nil, fmt.Errorf("search query failed: %s", resp.Status)
+ return nil, fmt.Errorf("search query failed: %s (body: %s)", resp.Status, body)
}
var result IssuesSearchResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {