whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,17 @@
.git
.localstorage
node_modules
devtools
.eslint*
.gitignore
.prettier*
.stylelint*
CONTRIBUTING.md
LICENSE
npm
npx
package-lock.json
package.json
PATENTS
README.md
tsconfig.json
@@ -0,0 +1,11 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"root": true,
"ignorePatterns": ["*.min.js"]
}
@@ -0,0 +1,14 @@
# Treat all files in the repo as binary, with no git magic updating
# line endings. This produces predictable results in different environments.
#
# Windows users contributing to Go will need to use a modern version
# of git and editors capable of LF line endings.
#
# Windows .bat files are known to have multiple bugs when run with LF
# endings. So if they are checked in with CRLF endings, there should
# be a test like the one in test/winbatch.go in the go repository.
# (See golang.org/issue/37791.)
#
# See golang.org/issue/9281.
* -text
@@ -0,0 +1,2 @@
node_modules
.localstorage
@@ -0,0 +1 @@
{"proseWrap": "always"}
@@ -0,0 +1,11 @@
{
"extends": ["stylelint-config-standard"],
"rules": {
"declaration-property-value-allowed-list": {
"/color/": ["/^var\\(--/", "transparent"]
},
"unit-disallowed-list": ["px"],
"selector-class-pattern": "^[a-zA-Z\\-]+$"
},
"ignoreFiles": ["**/*.min.css"]
}
@@ -0,0 +1,30 @@
# Contributing to Go
Go is an open source project.
It is the work of hundreds of contributors. We appreciate your help!
## Filing issues
When [filing an issue](https://golang.org/issue/new), make sure to answer these
five questions:
1. What version of Go are you using (`go version`)?
2. What operating system and processor architecture are you using?
3. What did you do?
4. What did you expect to see?
5. What did you see instead?
General questions should go to the
[golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead
of the issue tracker. The gophers there will answer or ask you to file an issue
if you've tripped over a bug.
## Contributing code
Please read the
[Contribution Guidelines](https://golang.org/doc/contribute.html) before sending
patches.
Unless otherwise noted, the Go source files are distributed under the BSD-style
license found in the LICENSE file.
@@ -0,0 +1,27 @@
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.
@@ -0,0 +1,60 @@
# Go Telemetry
This repository holds the Go Telemetry server code and libraries, used for
hosting [telemetry.go.dev](https://telemetry.go.dev) and instrumenting Go
toolchain programs with opt-in telemetry.
**Warning**: this repository is intended for use only in tools maintained by
the Go team, including tools in the Go distribution and auxiliary tools like
[gopls](https://pkg.go.dev/golang.org/x/tools/gopls) or
[govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck). There are
no compatibility guarantees for any of the packages here: public APIs will
change in breaking ways as the telemetry integration is refined.
## Notable Packages
- The [x/telemetry/counter](https://pkg.go.dev/golang.org/x/telemetry/counter)
package provides a library for instrumenting programs with counters and stack
reports.
- The [x/telemetry/upload](https://pkg.go.dev/golang.org/x/telemetry/upload)
package provides a hook for Go toolchain programs to upload telemetry data,
if the user has opted in to telemetry uploading.
- The [x/telemetry/cmd/gotelemetry](https://pkg.go.dev/pkg/golang.org/x/telemetry/cmd/gotelemetry)
command is used for managing telemetry data and configuration.
- The [x/telemetry/config](https://pkg.go.dev/pkg/golang.org/x/telemetry/config)
package defines the subset of telemetry data that has been approved for
uploading by the telemetry proposal process.
- The [x/telemetry/godev](https://pkg.go.dev/pkg/golang.org/x/telemetry/godev) directory defines
the services running at [telemetry.go.dev](https://telemetry.go.dev).
## Contributing
This repository uses Gerrit for code changes. To learn how to submit changes to
this repository, see https://golang.org/doc/contribute.html.
The main issue tracker for the time repository is located at
https://github.com/golang/go/issues. Prefix your issue with "x/telemetry:" in
the subject line, so it is easy to find.
### Linting & Formatting
This repository uses [eslint](https://eslint.org/) to format TS files,
[stylelint](https://stylelint.io/) to format CSS files, and
[prettier](https://prettier.io/) to format TS, CSS, Markdown, and YAML files.
See the style guides:
- [TypeScript](https://google.github.io/styleguide/tsguide.html)
- [CSS](https://go.dev/wiki/CSSStyleGuide)
It is encouraged that all TS and CSS code be run through formatters before
submitting a change. However, it is not a strict requirement enforced by CI.
### Installing npm Dependencies:
1. Install [docker](https://docs.docker.com/get-docker/)
2. Run `./npm install`
### Run ESLint, Stylelint, & Prettier
./npm run all
@@ -0,0 +1,30 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by TestDocHelp; DO NOT EDIT.
// Gotelemetry is a tool for managing Go telemetry data and settings.
//
// Usage:
//
// gotelemetry <command> [arguments]
//
// The commands are:
//
// on enable telemetry collection and uploading
// local enable telemetry collection but disable uploading
// off disable telemetry collection and uploading
// view run a web viewer for local telemetry data
// env print the current telemetry environment
// clean remove all local telemetry data
//
// Use "gotelemetry help <command>" for details about any command.
//
// The following additional commands are available for diagnostic
// purposes, and may change or be removed in the future:
//
// csv print all known counters
// dump view counter file data
// upload run upload with logging enabled
package main
@@ -0,0 +1,82 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"flag"
"fmt"
"go/format"
"go/parser"
"go/token"
"os"
"os/exec"
"strings"
"testing"
"golang.org/x/telemetry/internal/testenv"
)
var updateDocs = flag.Bool("update", false, "if set, update docs")
func TestMain(m *testing.M) {
if os.Getenv("GOTELEMETRY_RUN_AS_MAIN") != "" {
main()
os.Exit(0)
}
os.Exit(m.Run())
}
func TestDocHelp(t *testing.T) {
testenv.MustHaveExec(t)
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(exe, "help")
cmd.Env = append(os.Environ(), "GOTELEMETRY_RUN_AS_MAIN=1")
help, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
if *updateDocs {
var lines []string
for _, line := range strings.Split(strings.TrimSpace(string(help)), "\n") {
if len(line) > 0 {
lines = append(lines, "// "+line)
} else {
lines = append(lines, "//")
}
}
contents := fmt.Sprintf(`// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by TestDocHelp; DO NOT EDIT.
%s
package main
`, strings.Join(lines, "\n"))
data, err := format.Source([]byte(contents))
if err != nil {
t.Fatalf("formatting content: %v", err)
}
if err := os.WriteFile("doc.go", data, 0666); err != nil {
t.Fatalf("writing doc.go: %v", err)
}
}
f, err := parser.ParseFile(token.NewFileSet(), "doc.go", nil, parser.PackageClauseOnly|parser.ParseComments)
if err != nil {
t.Fatalf("parsing doc.go: %v", err)
}
doc := f.Doc.Text()
if got, want := doc, string(help); got != want {
t.Errorf("doc.go: mismatching content\ngot:\n%s\nwant:\n%s", got, want)
}
}
@@ -0,0 +1,68 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package browser provides utilities for interacting with users' browsers.
// This is a copy of the go project's src/cmd/internal/browser.
package browser
import (
"os"
"os/exec"
"runtime"
"time"
)
// Commands returns a list of possible commands to use to open a url.
func Commands() [][]string {
var cmds [][]string
if exe := os.Getenv("BROWSER"); exe != "" {
cmds = append(cmds, []string{exe})
}
switch runtime.GOOS {
case "darwin":
cmds = append(cmds, []string{"/usr/bin/open"})
case "windows":
cmds = append(cmds, []string{"cmd", "/c", "start"})
default:
if os.Getenv("DISPLAY") != "" {
// xdg-open is only for use in a desktop environment.
cmds = append(cmds, []string{"xdg-open"})
}
}
cmds = append(cmds,
[]string{"chrome"},
[]string{"google-chrome"},
[]string{"chromium"},
[]string{"firefox"},
)
return cmds
}
// Open tries to open url in a browser and reports whether it succeeded.
func Open(url string) bool {
for _, args := range Commands() {
cmd := exec.Command(args[0], append(args[1:], url)...)
if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) {
return true
}
}
return false
}
// appearsSuccessful reports whether the command appears to have run successfully.
// If the command runs longer than the timeout, it's deemed successful.
// If the command runs within the timeout, it's deemed successful if it exited cleanly.
func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool {
errc := make(chan error, 1)
go func() {
errc <- cmd.Wait()
}()
select {
case <-time.After(timeout):
return true
case err := <-errc:
return err == nil
}
}
@@ -0,0 +1,142 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// csv dumps all the active counters. The output is
// a sequence of lines
// value,"counter-name",program, version,go-version,goos, garch
// sorted by counter name. It looks at the files in
// telemetry.LocalDir that are counter files or local reports
// By design it pays no attention to dates. The combination
// of program version and go version are deemed sufficient.
package csv
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
)
type file struct {
path, name string
// one of counters or report is set
counters *counter.File
report *telemetry.Report
}
func Csv() {
files, err := readdir(telemetry.Default.LocalDir(), nil)
if err != nil {
log.Fatal(err)
}
for _, f := range files {
if strings.HasSuffix(f.name, "v1.count") {
buf, err := os.ReadFile(f.path)
if err != nil {
log.Print(err)
continue
}
cf, err := counter.Parse(f.name, buf)
if err != nil {
log.Print(err)
continue
}
f.counters = cf
} else if strings.HasSuffix(f.name, ".json") {
buf, err := os.ReadFile(f.path)
if err != nil {
log.Print(err)
continue
}
var x telemetry.Report
if err := json.Unmarshal(buf, &x); err != nil {
log.Print(err)
continue
}
f.report = &x
}
}
printTable(files)
}
type record struct {
goos, garch, program, version, goversion string
cntr string
count int
}
func printTable(files []*file) {
lines := make(map[string]*record)
work := func(k string, v int64, rec *record) {
x, ok := lines[k]
if !ok {
x = new(record)
*x = *rec
x.cntr = k
}
x.count += int(v)
lines[k] = x
}
worku := func(k string, v uint64, rec *record) {
work(k, int64(v), rec)
}
for _, f := range files {
if f.counters != nil {
var rec record
rec.goos = f.counters.Meta["GOOS"]
rec.garch = f.counters.Meta["GOARCH"]
rec.program = f.counters.Meta["Program"]
rec.version = f.counters.Meta["Version"]
rec.goversion = f.counters.Meta["GoVersion"]
for k, v := range f.counters.Count {
worku(k, v, &rec)
}
} else if f.report != nil {
for _, p := range f.report.Programs {
var rec record
rec.goos = p.GOOS
rec.garch = p.GOARCH
rec.goversion = p.GoVersion
rec.program = p.Program
rec.version = p.Version
for k, v := range p.Counters {
work(k, v, &rec)
}
for k, v := range p.Stacks {
work(k, v, &rec)
}
}
}
}
keys := make([]string, 0, len(lines))
for k := range lines {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
printRecord(lines[k])
}
}
func printRecord(r *record) {
fmt.Printf("%d,%q,%s,%s,%s,%s,%s\n", r.count, r.cntr, r.program,
r.version, r.goversion, r.goos, r.garch)
}
func readdir(dir string, files []*file) ([]*file, error) {
fi, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, f := range fi {
files = append(files, &file{path: filepath.Join(dir, f.Name()), name: f.Name()})
}
return files, nil
}
@@ -0,0 +1,18 @@
# Go Telemetry View
Telemetry data it is stored in files on the user machine. Users can run the
command `gotelemetry view` to view the data in a browser. The HTML page served
by the command will generate graphs based on the local copies of report uploads
and active counter files.
## Development
The static files are generated with a generator command. You can edit the source
files and run go generate to rebuild them.
go generate ./content
Running the server with the `--dev` flag will watch and rebuild the static files
on save.
go run ./cmd/gotelemetry view --dev
@@ -0,0 +1,675 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The view command is a server intended to be run on a user's machine to
// display the local counters and time series charts of counters.
package view
import (
"bytes"
"encoding/json"
"fmt"
"html"
"html/template"
"io/fs"
"log"
"net"
"net/http"
"os"
"path"
"sort"
"strconv"
"strings"
"time"
"golang.org/x/telemetry/cmd/gotelemetry/internal/browser"
"golang.org/x/telemetry/internal/config"
"golang.org/x/telemetry/internal/configstore"
contentfs "golang.org/x/telemetry/internal/content"
tcounter "golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/unionfs"
)
type Server struct {
Addr string
Dev bool
FsConfig string
Open bool
}
// Serve starts the telemetry viewer and runs indefinitely.
func (s *Server) Serve() {
var fsys fs.FS = contentfs.FS
if s.Dev {
fsys = os.DirFS("internal/content")
contentfs.RunESBuild(true)
}
var err error
fsys, err = unionfs.Sub(fsys, "gotelemetryview", "shared")
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.Handle("/", s.handleIndex(fsys))
listener, err := net.Listen("tcp", s.Addr)
if err != nil {
log.Fatal(err)
}
addr := fmt.Sprintf("http://%s", listener.Addr())
fmt.Printf("server listening at %s\n", addr)
if s.Open {
browser.Open(addr)
}
log.Fatal(http.Serve(listener, mux))
}
type page struct {
// Config is the config used to render the requested page.
Config *config.Config
// PrettyConfig is the Config struct formatted as indented JSON for display on the page.
PrettyConfig string
// ConfigVersion is used to render a dropdown list of config versions for a user to select.
ConfigVersions []string
// RequestedConfig is the URL query param value for config.
RequestedConfig string
// Files are the local counter files for display on the page.
Files []*counterFile
// Reports are the local reports for display on the page.
Reports []*telemetryReport
// Charts is the counter data from files and reports grouped by program and counter name.
Charts *chartdata
}
// TODO: filtering and pagination for date ranges
func (s *Server) handleIndex(fsys fs.FS) handlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
if r.URL.Path != "/" {
http.FileServer(http.FS(fsys)).ServeHTTP(w, r)
return nil
}
requestedConfig := r.URL.Query().Get("config")
if requestedConfig == "" {
requestedConfig = "latest"
}
cfg, err := s.configAt(requestedConfig)
if err != nil {
log.Printf("Falling back to empty config: %v", err)
cfg, _ = s.configAt("empty")
}
cfgVersionList, err := configVersions()
if err != nil {
return err
}
cfgJSON, err := json.MarshalIndent(cfg, "", "\t")
if err != nil {
return err
}
localDir := telemetry.Default.LocalDir()
if _, err := os.Stat(localDir); err != nil {
return fmt.Errorf(
`The telemetry dir %s does not exist.
There is nothing to report.`, telemetry.Default.LocalDir())
}
reports, err := reports(localDir, cfg)
if err != nil {
return err
}
files, err := files(localDir, cfg)
if err != nil {
return err
}
charts, err := charts(append(reports, pending(files, cfg)...), cfg)
if err != nil {
return err
}
data := page{
Config: cfg,
PrettyConfig: string(cfgJSON),
ConfigVersions: cfgVersionList,
Reports: reports,
Files: files,
Charts: charts,
RequestedConfig: requestedConfig,
}
return renderTemplate(w, fsys, "index.html", data, http.StatusOK)
}
}
// configAt gets the config at a given version.
func (s Server) configAt(version string) (ucfg *config.Config, err error) {
if version == "" || version == "empty" {
return config.NewConfig(&telemetry.UploadConfig{}), nil
}
if s.FsConfig != "" {
ucfg, err = config.ReadConfig(s.FsConfig)
if err != nil {
return nil, err
}
} else {
cfg, _, err := configstore.Download(version, nil)
if err != nil {
return nil, err
}
ucfg = config.NewConfig(cfg)
}
return ucfg, nil
}
// configVersions is the set of config versions the user may select from the UI.
// TODO: get the list of versions available from the proxy.
func configVersions() ([]string, error) {
v := []string{"latest"}
return v, nil
}
// reports reads the local report files from a directory.
func reports(dir string, cfg *config.Config) ([]*telemetryReport, error) {
fsys := os.DirFS(dir)
entries, err := fs.ReadDir(fsys, ".")
if err != nil {
return nil, err
}
var reports []*telemetryReport
for _, e := range entries {
if path.Ext(e.Name()) != ".json" {
continue
}
data, err := fs.ReadFile(fsys, e.Name())
if err != nil {
log.Printf("read report file failed: %v", err)
continue
}
var report *telemetry.Report
if err := json.Unmarshal(data, &report); err != nil {
log.Printf("unmarshal report file %v failed: %v, skipping...", e.Name(), err)
continue
}
wrapped, err := newTelemetryReport(report, cfg)
if err != nil {
log.Printf("processing report file %v failed: %v, skipping", e.Name(), err)
continue
}
reports = append(reports, wrapped)
}
// sort the reports descending by week.
sort.Slice(reports, func(i, j int) bool {
return reports[j].Week < reports[i].Week
})
return reports, nil
}
// telemetryReport wraps telemetry report to add convenience fields for the UI.
type telemetryReport struct {
*telemetry.Report
ID string
WeekEnd time.Time // parsed telemetry.Report.Week
Programs []*telemetryProgram
}
type telemetryProgram struct {
*telemetry.ProgramReport
ID string
Summary template.HTML
}
func newTelemetryReport(t *telemetry.Report, cfg *config.Config) (*telemetryReport, error) {
weekEnd, err := parseReportDate(t.Week)
if err != nil {
return nil, fmt.Errorf("unexpected Week %q in the report", t.Week)
}
var prgms []*telemetryProgram
for _, p := range t.Programs {
meta := map[string]string{
"Program": p.Program,
"Version": p.Version,
"GOOS": p.GOOS,
"GOARCH": p.GOARCH,
"GoVersion": p.GoVersion,
}
counters := make(map[string]uint64)
for k, v := range p.Counters {
counters[k] = uint64(v)
}
prgms = append(prgms, &telemetryProgram{
ProgramReport: p,
ID: strings.Join([]string{"reports", t.Week, p.Program, p.Version, p.GOOS, p.GOARCH, p.GoVersion}, ":"),
Summary: summary(cfg, meta, counters),
})
}
return &telemetryReport{
Report: t,
WeekEnd: weekEnd,
ID: "reports:" + t.Week,
Programs: prgms,
}, nil
}
// files reads the local counter files from a directory.
func files(dir string, cfg *config.Config) ([]*counterFile, error) {
fsys := os.DirFS(dir)
entries, err := fs.ReadDir(fsys, ".")
if err != nil {
return nil, err
}
var files []*counterFile
for _, e := range entries {
if e.IsDir() || path.Ext(e.Name()) != ".count" {
continue
}
data, err := fs.ReadFile(fsys, e.Name())
if err != nil {
log.Printf("read counter file failed: %v", err)
continue
}
file, err := tcounter.Parse(e.Name(), data)
if err != nil {
log.Printf("parse counter file failed: %v", err)
continue
}
files = append(files, newCounterFile(e.Name(), file, cfg))
}
return files, nil
}
// counterFile wraps counter file to add convenience fields for the UI.
type counterFile struct {
*tcounter.File
ID string
Summary template.HTML
ActiveMeta map[string]bool
Counts []*count
Stacks []*stack
}
type count struct {
Name string
Value uint64
Active bool
}
type stack struct {
Name string
Trace string
Value uint64
Active bool
}
func newCounterFile(name string, c *tcounter.File, cfg *config.Config) *counterFile {
activeMeta := map[string]bool{
"Program": cfg.HasProgram(c.Meta["Program"]),
"Version": cfg.HasVersion(c.Meta["Program"], c.Meta["Version"]),
"GOOS": cfg.HasGOOS(c.Meta["GOOS"]),
"GOARCH": cfg.HasGOARCH(c.Meta["GOARCH"]),
"GoVersion": cfg.HasGoVersion(c.Meta["GoVersion"]),
}
var counts []*count
var stacks []*stack
for k, v := range c.Count {
if summary, details, ok := strings.Cut(k, "\n"); ok {
active := cfg.HasStack(c.Meta["Program"], k)
stacks = append(stacks, &stack{summary, details, v, active})
} else {
active := cfg.HasCounter(c.Meta["Program"], k)
counts = append(counts, &count{k, v, active})
}
}
sort.Slice(counts, func(i, j int) bool {
return counts[i].Name < counts[j].Name
})
sort.Slice(stacks, func(i, j int) bool {
return stacks[i].Name < stacks[j].Name
})
return &counterFile{
File: c,
ID: name,
ActiveMeta: activeMeta,
Counts: counts,
Stacks: stacks,
Summary: summary(cfg, c.Meta, c.Count),
}
}
// summary generates a summary of a set of telemetry data. It describes what data is
// located in the set is not allowed given a config and how the data would be handled
// in the event of a telemetry upload event.
func summary(cfg *config.Config, meta map[string]string, counts map[string]uint64) template.HTML {
msg := " is unregistered. No data from this set would be uploaded to the Go team."
if prog := meta["Program"]; !(cfg.HasProgram(prog)) {
return template.HTML(fmt.Sprintf(
"The program <code>%s</code>"+msg,
html.EscapeString(prog),
))
}
var result strings.Builder
if !(cfg.HasGOOS(meta["GOOS"])) || !(cfg.HasGOARCH(meta["GOARCH"])) {
return template.HTML(fmt.Sprintf(
"The GOOS/GOARCH combination <code>%s/%s</code> "+msg,
html.EscapeString(meta["GOOS"]),
html.EscapeString(meta["GOARCH"]),
))
}
goVersion := meta["GoVersion"]
if !(cfg.HasGoVersion(goVersion)) {
return template.HTML(fmt.Sprintf(
"The go version <code>%s</code> "+msg,
html.EscapeString(goVersion),
))
}
version := meta["Version"]
if !(cfg.HasVersion(meta["Program"], version)) {
return template.HTML(fmt.Sprintf(
"The version <code>%s</code> "+msg,
html.EscapeString(version),
))
}
var counters []string
for c := range counts {
summary, _, ok := strings.Cut(c, "\n")
if ok && !cfg.HasStack(meta["Program"], c) {
counters = append(counters, fmt.Sprintf("<code>%s</code>", html.EscapeString(summary)))
}
if !ok && !(cfg.HasCounter(meta["Program"], c)) {
counters = append(counters, fmt.Sprintf("<code>%s</code>", html.EscapeString(c)))
}
}
if len(counters) > 0 {
result.WriteString("Unregistered counter(s) ")
result.WriteString(strings.Join(counters, ", "))
result.WriteString(" would be excluded from a report. ")
}
return template.HTML(result.String())
}
type chartdata struct {
Programs []*program
// DateRange is used to align the week intervals for each of the charts.
DateRange [2]string
// UploadDay is the day of the week the reports are uploaded.
// This is used as d3 chart time interval name
// to customize the date range bining in the charts.
UploadDay string
}
type program struct {
ID string
Name string
Counters []*counter
Active bool
}
type counter struct {
ID string
Name string
Data []*datum
Active bool
}
type datum struct {
Week string // End of the week in UTC. YYYY-MM-DDT00:00:00Z format.
Program string
Version string
GOARCH string
GOOS string
GoVersion string
Key string
Value int64
}
// formatDateTime formats the date to the format that
// includes time zone. Telemetry uses UTC for date string
// parsing, but JavaScript Date parsing uses local time
// unless the date string include the time zone info.
func formatDateTime(date time.Time) string {
return date.Format("2006-01-02T15:04:05Z") // UTC
}
// parseReportDate parses the date string in the format
// used byt the telemetry report.
func parseReportDate(s string) (time.Time, error) {
return time.Parse(telemetry.DateOnly, s)
}
// charts returns chartdata for a set of telemetry reports. It uses the config
// to determine if the programs and counters are active.
func charts(reports []*telemetryReport, cfg *config.Config) (*chartdata, error) {
data := grouped(reports)
// domain is a [min, max] array used in d3.js where min is the minimum
// observable time and max is the maximum observable time; both values
// are inclusive.
domain, err := reportsDomain(reports)
if err != nil {
return nil, err
}
result := &chartdata{
DateRange: [2]string{formatDateTime(domain[0]), formatDateTime(domain[1])},
UploadDay: strings.ToLower(domain[1].Weekday().String()),
}
for pg, pgdata := range data {
prog := &program{ID: "charts:" + pg.Name, Name: pg.Name, Active: cfg.HasProgram(pg.Name)}
result.Programs = append(result.Programs, prog)
for c, cdata := range pgdata {
count := &counter{
ID: "charts:" + pg.Name + ":" + c.Name,
Name: c.Name,
Data: cdata,
Active: cfg.HasCounter(pg.Name, c.Name) || cfg.HasCounterPrefix(pg.Name, c.Name),
}
prog.Counters = append(prog.Counters, count)
sort.Slice(count.Data, func(i, j int) bool {
a, err1 := strconv.ParseFloat(count.Data[i].Key, 32)
b, err2 := strconv.ParseFloat(count.Data[j].Key, 32)
if err1 == nil && err2 == nil {
return a < b
}
return count.Data[i].Key < count.Data[j].Key
})
}
sort.Slice(prog.Counters, func(i, j int) bool {
return prog.Counters[i].Name < prog.Counters[j].Name
})
}
sort.Slice(result.Programs, func(i, j int) bool {
return result.Programs[i].Name < result.Programs[j].Name
})
return result, nil
}
// reportsDomain computes a common reportsDomain.
func reportsDomain(reports []*telemetryReport) ([2]time.Time, error) {
var start, end time.Time
for _, r := range reports {
if start.IsZero() || start.After(r.WeekEnd) {
start = r.WeekEnd
}
if end.IsZero() || r.WeekEnd.After(end) {
end = r.WeekEnd
}
}
if start.IsZero() || end.IsZero() {
return [2]time.Time{}, fmt.Errorf("no report with valid Week data")
}
start = start.AddDate(0, 0, -7) // 7 days before the first report.
return [2]time.Time{start, end}, nil
}
type programKey struct {
Name string
}
type counterKey struct {
Name string
}
// grouped returns normalized counter data grouped by program and counter.
func grouped(reports []*telemetryReport) map[programKey]map[counterKey][]*datum {
result := make(map[programKey]map[counterKey][]*datum)
for _, r := range reports {
// Adjust the Week string to include the time zone info.
// JS's Date.parse uses local time, otherwise.
//
// r.Week is the end of the week interval in UTC.
// If r.Week is 2024-01-08, the report is the data
// for the d3 domain[2024-01-01T00:00:00Z, 2024-01-08T00:00:00Z).
// Note: the end is exclusive.
// To make the report data align with the d3 domain,
// adjust the time to the start of the week interval.
weekStart := formatDateTime(r.WeekEnd.AddDate(0, 0, -7))
for _, e := range r.Programs {
pgkey := programKey{e.Program}
if _, ok := result[pgkey]; !ok {
result[pgkey] = make(map[counterKey][]*datum)
}
for counter, value := range e.Counters {
name, bucket, found := strings.Cut(counter, ":")
key := name
if found {
key = bucket
}
element := &datum{
Week: weekStart,
Program: e.Program,
Version: e.Version,
GOARCH: e.GOARCH,
GOOS: e.GOOS,
GoVersion: e.GoVersion,
Key: key,
Value: value,
}
ckey := counterKey{name}
result[pgkey][ckey] = append(result[pgkey][ckey], element)
}
for counter, value := range e.Stacks {
summary, _, _ := strings.Cut(counter, "\n")
element := &datum{
Week: weekStart,
Program: e.Program,
Version: e.Version,
GOARCH: e.GOARCH,
GOOS: e.GOOS,
GoVersion: e.GoVersion,
Key: summary,
Value: value,
}
ckey := counterKey{summary}
result[pgkey][ckey] = append(result[pgkey][ckey], element)
}
}
}
return result
}
// pending transforms the active counter files into a report. Used to add
// the data they contain to the charts in the UI.
func pending(files []*counterFile, cfg *config.Config) []*telemetryReport {
reports := make(map[string]*telemetry.Report)
for _, f := range files {
tb, err := time.Parse(time.RFC3339, f.Meta["TimeEnd"])
if err != nil {
log.Printf("skipping malformed %v: unexpected TimeEnd value %q", f.ID, f.Meta["TimeEnd"])
continue
}
week := tb.Format(telemetry.DateOnly)
if _, ok := reports[week]; !ok {
reports[week] = &telemetry.Report{Week: week}
}
program := &telemetry.ProgramReport{
Program: f.Meta["Program"],
GOOS: f.Meta["GOOS"],
GOARCH: f.Meta["GOARCH"],
GoVersion: f.Meta["GoVersion"],
Version: f.Meta["Version"],
}
program.Counters = make(map[string]int64)
program.Stacks = make(map[string]int64)
for k, v := range f.Count {
if tcounter.IsStackCounter(k) {
program.Stacks[k] = int64(v)
} else {
program.Counters[k] = int64(v)
}
}
reports[week].Programs = append(reports[week].Programs, program)
}
var result []*telemetryReport
for _, r := range reports {
wrapped, err := newTelemetryReport(r, cfg)
if err != nil {
log.Printf("skipping the invalid report from week %v: %v", r.Week, err)
continue
}
result = append(result, wrapped)
}
return result
}
type handlerFunc func(http.ResponseWriter, *http.Request) error
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := f(w, r); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// renderTemplate executes a template response.
func renderTemplate(w http.ResponseWriter, fsys fs.FS, tmplPath string, data any, code int) error {
patterns, err := tmplPatterns(fsys, tmplPath)
if err != nil {
return err
}
patterns = append(patterns, tmplPath)
funcs := template.FuncMap{
"chartName": func(name string) string {
name, _, _ = strings.Cut(name, ":")
return name
},
"programName": func(name string) string {
name = strings.TrimPrefix(name, "golang.org/")
name = strings.TrimPrefix(name, "github.com/")
return name
},
}
tmpl, err := template.New("").Funcs(funcs).ParseFS(fsys, patterns...)
if err != nil {
return err
}
name := path.Base(tmplPath)
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
return err
}
if code != 0 {
w.WriteHeader(code)
}
w.Header().Set("Content-Type", "text/html")
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
if _, err := w.Write(buf.Bytes()); err != nil {
return err
}
return nil
}
// tmplPatterns generates a slice of file patterns to use in template.ParseFS.
func tmplPatterns(fsys fs.FS, tmplPath string) ([]string, error) {
var patterns []string
globs := []string{"*.tmpl", path.Join(path.Dir(tmplPath), "*.tmpl")}
for _, g := range globs {
matches, err := fs.Glob(fsys, g)
if err != nil {
return nil, err
}
patterns = append(patterns, matches...)
}
return patterns, nil
}
@@ -0,0 +1,156 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The view command is a server intended to be run on a users machine to
// display the local counters and time series charts of counters.
package view
import (
"fmt"
"html/template"
"reflect"
"testing"
"time"
"golang.org/x/telemetry/internal/config"
"golang.org/x/telemetry/internal/telemetry"
)
func Test_summary(t *testing.T) {
type args struct {
cfg *config.Config
meta map[string]string
counts map[string]uint64
}
cfg := config.NewConfig(&telemetry.UploadConfig{
GOOS: []string{"linux"},
GOARCH: []string{"amd64"},
GoVersion: []string{"go1.20.1"},
Programs: []*telemetry.ProgramConfig{
{
Name: "gopls",
Versions: []string{"v1.2.3"},
Counters: []telemetry.CounterConfig{
{Name: "editor"},
},
},
},
})
tests := []struct {
name string
args args
want template.HTML
}{
{
"empty summary",
args{
cfg: cfg,
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.20.1"},
counts: map[string]uint64{"editor": 10},
},
template.HTML(""),
},
{
"empty config/unknown program",
args{
cfg: config.NewConfig(&telemetry.UploadConfig{}),
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.20.1"},
counts: map[string]uint64{"editor": 10},
},
template.HTML("The program <code>gopls</code> is unregistered. No data from this set would be uploaded to the Go team."),
},
{
"unknown counter",
args{
cfg: cfg,
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.20.1"},
counts: map[string]uint64{"editor": 10, "foobar": 10},
},
template.HTML("Unregistered counter(s) <code>foobar</code> would be excluded from a report. "),
},
{
"unknown goos",
args{
cfg: cfg,
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "windows", "GOARCH": "arm64", "GoVersion": "go1.20.1"},
counts: map[string]uint64{"editor": 10, "foobar": 10},
},
template.HTML("The GOOS/GOARCH combination <code>windows/arm64</code> is unregistered. No data from this set would be uploaded to the Go team."),
},
{
"multiple unknown fields",
args{
cfg: cfg,
meta: map[string]string{"Program": "gopls", "Version": "v1.2.5", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.25.1"},
counts: map[string]uint64{"editor": 10, "foobar": 10},
},
template.HTML("The go version <code>go1.25.1</code> is unregistered. No data from this set would be uploaded to the Go team."),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := summary(tt.args.cfg, tt.args.meta, tt.args.counts)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("summary() = %q, want %q", got, tt.want)
}
})
}
}
func Test_reportsDomain(t *testing.T) {
mustParseDate := func(date string) time.Time {
ts, err := time.Parse(telemetry.DateOnly, date)
if err != nil {
t.Fatalf("failed to parse date %q: %v", date, err)
}
return ts
}
tests := []struct {
name string
reportDates []string
want [2]time.Time
wantErr bool
}{
{
name: "empty",
wantErr: true,
},
{
name: "one",
reportDates: []string{"2024-01-08"},
want: [2]time.Time{mustParseDate("2024-01-01"), mustParseDate("2024-01-08")},
},
{
name: "two",
reportDates: []string{"2024-04-08", "2024-06-01"},
want: [2]time.Time{mustParseDate("2024-04-01"), mustParseDate("2024-06-01")},
},
{
name: "three",
reportDates: []string{"2024-04-08", "2024-01-08", "2024-06-01"},
want: [2]time.Time{mustParseDate("2024-01-01"), mustParseDate("2024-06-01")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reports := make([]*telemetryReport, len(tt.reportDates))
for i, date := range tt.reportDates {
weekEnd, err := parseReportDate(date)
if err != nil {
t.Fatalf("parseReport(%v) failed: %v", date, err)
}
reports[i] = &telemetryReport{
WeekEnd: weekEnd,
ID: fmt.Sprintf("report-%d", i),
}
}
got, err := reportsDomain(reports)
if tt.wantErr && err == nil ||
err == nil && !reflect.DeepEqual(got, tt.want) {
t.Errorf("reportsDomain() = (%v, %v), want (%v, err=%v)", got, err, tt.want, tt.wantErr)
}
})
}
}
@@ -0,0 +1,395 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go test -run=TestDocHelp -update
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"golang.org/x/telemetry/cmd/gotelemetry/internal/csv"
"golang.org/x/telemetry/cmd/gotelemetry/internal/view"
"golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/upload"
)
type command struct {
usage string
short string
long string
flags *flag.FlagSet
hasArgs bool
run func([]string)
}
func (c command) name() string {
name, _, _ := strings.Cut(c.usage, " ")
return name
}
var (
viewFlags = flag.NewFlagSet("view", flag.ExitOnError)
viewServer view.Server
normalCommands = []*command{
{
usage: "on",
short: "enable telemetry collection and uploading",
long: `Gotelemetry on enables telemetry collection and uploading.
When telemetry is enabled, telemetry data is written to the local file system and periodically sent to https://telemetry.go.dev/. Uploaded data is used to help improve the Go toolchain and related tools, and it will be published as part of a public dataset.
For more details, see https://telemetry.go.dev/privacy.
This data is collected in accordance with the Google Privacy Policy (https://policies.google.com/privacy).
To disable telemetry uploading, but keep local data collection, run “gotelemetry local”.
To disable both collection and uploading, run “gotelemetry off“.
`,
run: runOn,
},
{
usage: "local",
short: "enable telemetry collection but disable uploading",
long: `Gotelemetry local enables telemetry collection but not uploading.
When telemetry is in local mode, counter data is written to the local file system, but will not be uploaded to remote servers.
To enable telemetry uploading, run “gotelemetry on”.
To disable both collection and uploading, run “gotelemetry off”`,
run: runLocal,
},
{
usage: "off",
short: "disable telemetry collection and uploading",
long: `Gotelemetry off disables telemetry collection and uploading.
When telemetry is disabled, local counter data is neither collected nor uploaded.
To enable local collection (but not uploading) of telemetry data, run “gotelemetry local“.
To enable both collection and uploading, run “gotelemetry on”.`,
run: runOff,
},
{
usage: "view [flags]",
short: "run a web viewer for local telemetry data",
long: `Gotelemetry view runs a web viewer for local telemetry data.
This viewer displays charts for locally collected data, as well as information about the current upload configuration.`,
flags: viewFlags,
run: runView,
},
{
usage: "env",
short: "print the current telemetry environment",
run: runEnv,
},
{
usage: "clean",
short: "remove all local telemetry data",
long: `Gotelemetry clean removes locally collected counters and reports.
Removing counter files that are currently in use may fail on some operating
systems.
Gotelemetry clean does not affect the current telemetry mode.`,
run: runClean,
},
}
experimentalCommands = []*command{
{
usage: "csv",
short: "print all known counters",
run: runCSV,
},
{
usage: "dump [files]",
short: "view counter file data",
run: runDump,
hasArgs: true,
},
{
usage: "upload",
short: "run upload with logging enabled",
run: runUpload,
},
}
)
func init() {
viewFlags.StringVar(&viewServer.Addr, "addr", "localhost:4040", "server listens on the given TCP network address")
viewFlags.BoolVar(&viewServer.Dev, "dev", false, "rebuild static assets on save")
viewFlags.StringVar(&viewServer.FsConfig, "config", "", "load a config from the filesystem")
viewFlags.BoolVar(&viewServer.Open, "open", true, "open the browser to the server address")
for _, cmd := range append(normalCommands, experimentalCommands...) {
name := cmd.name()
if cmd.flags == nil {
cmd.flags = flag.NewFlagSet(name, flag.ExitOnError)
}
cmd.flags.Usage = func() {
help(name)
}
}
}
func output(msgs ...any) {
fmt.Fprintln(flag.CommandLine.Output(), msgs...)
}
func usage() {
printCommand := func(cmd *command) {
output(fmt.Sprintf("\t%s\t%s", cmd.name(), cmd.short))
}
output("Gotelemetry is a tool for managing Go telemetry data and settings.")
output()
output("Usage:")
output()
output("\tgotelemetry <command> [arguments]")
output()
output("The commands are:")
output()
for _, cmd := range normalCommands {
printCommand(cmd)
}
output()
output("Use \"gotelemetry help <command>\" for details about any command.")
output()
output("The following additional commands are available for diagnostic")
output("purposes, and may change or be removed in the future:")
output()
for _, cmd := range experimentalCommands {
printCommand(cmd)
}
}
func failf(format string, args ...any) {
fmt.Fprintf(os.Stderr, format, args...)
os.Exit(1)
}
func warnf(format string, args ...any) {
fmt.Fprintf(os.Stderr, "Warning: "+format+"\n", args...)
}
func findCommand(name string) *command {
for _, cmd := range append(normalCommands, experimentalCommands...) {
if cmd.name() == name {
return cmd
}
}
return nil
}
func help(name string) {
cmd := findCommand(name)
if cmd == nil {
failf("unknown command %q", name)
}
output(fmt.Sprintf("Usage: gotelemetry %s", cmd.usage))
output()
if cmd.long != "" {
output(cmd.long)
} else {
output(fmt.Sprintf("Gotelemetry %s is used to %s.", cmd.name(), cmd.short))
}
anyflags := false
cmd.flags.VisitAll(func(*flag.Flag) {
anyflags = true
})
if anyflags {
output()
output("Flags:")
output()
cmd.flags.PrintDefaults()
}
}
func runOn(_ []string) {
if old, _ := telemetry.Default.Mode(); old == "on" {
return
}
if err := telemetry.Default.SetMode("on"); err != nil {
failf("Failed to enable telemetry: %v", err)
}
// We could perhaps only show the telemetry on message when the mode goes
// from off->on (i.e. check the previous state before calling setMode),
// but that seems like an unnecessary optimization.
fmt.Fprintln(os.Stderr, telemetryOnMessage())
}
func telemetryOnMessage() string {
return `Telemetry uploading is now enabled.
Data will be sent periodically to https://telemetry.go.dev/.
Uploaded data is used to help improve the Go toolchain and related tools,
and it will be published as part of a public dataset.
For more details, see https://telemetry.go.dev/privacy.
This data is collected in accordance with the Google Privacy Policy
(https://policies.google.com/privacy).
To disable telemetry uploading, but keep local data collection,
run “gotelemetry local”.
To disable both collection and uploading, run “gotelemetry off“.`
}
func runLocal(_ []string) {
if old, _ := telemetry.Default.Mode(); old == "local" {
return
}
if err := telemetry.Default.SetMode("local"); err != nil {
failf("Failed to set the telemetry mode to local: %v", err)
}
}
func runOff(_ []string) {
if old, _ := telemetry.Default.Mode(); old == "off" {
return
}
if err := telemetry.Default.SetMode("off"); err != nil {
failf("Failed to disable telemetry: %v", err)
}
}
func runView(_ []string) {
viewServer.Serve()
}
func runEnv(_ []string) {
m, t := telemetry.Default.Mode()
fmt.Printf("mode: %s %s\n", m, t)
fmt.Println()
fmt.Println("modefile:", telemetry.Default.ModeFile())
fmt.Println("localdir:", telemetry.Default.LocalDir())
fmt.Println("uploaddir:", telemetry.Default.UploadDir())
}
func runClean(_ []string) {
// For now, be careful to only remove counter files and reports.
// It would probably be OK to just remove everything, but it may
// be useful to preserve the weekends file.
for dir, suffixes := range map[string][]string{
telemetry.Default.LocalDir(): {"." + counter.FileVersion + ".count", ".json"},
telemetry.Default.UploadDir(): {".json"},
} {
entries, err := os.ReadDir(dir)
if err != nil {
if !os.IsNotExist(err) {
warnf("failed to read telemetry dir: %v", err)
}
continue
}
for _, entry := range entries {
// TODO: use slices.ContainsFunc once it is available in all supported Go
// versions.
remove := false
for _, suffix := range suffixes {
if strings.HasSuffix(entry.Name(), suffix) {
remove = true
break
}
}
if remove {
path := filepath.Join(dir, entry.Name())
if err := os.Remove(path); err != nil {
warnf("failed to remove %s: %v", path, err)
}
}
}
}
}
func runCSV(_ []string) {
csv.Csv()
}
func runDump(args []string) {
if len(args) == 0 {
localdir := telemetry.Default.LocalDir()
fi, err := os.ReadDir(localdir)
if err != nil && len(args) == 0 {
log.Fatal(err)
}
for _, f := range fi {
args = append(args, filepath.Join(localdir, f.Name()))
}
}
for _, file := range args {
if !strings.HasSuffix(file, ".count") {
log.Printf("%s: not a counter file, skipping", file)
continue
}
data, err := os.ReadFile(file)
if err != nil {
log.Printf("%v, skipping", err)
continue
}
f, err := counter.Parse(file, data)
if err != nil {
log.Printf("%v, skipping", err)
continue
}
js, err := json.MarshalIndent(f, "", "\t")
if err != nil {
log.Printf("%s: failed to print - %v", file, err)
}
fmt.Printf("-- %v --\n%s\n", file, js)
}
}
func runUpload(_ []string) {
if err := upload.Run(upload.RunConfig{
LogWriter: os.Stderr,
}); err != nil {
fmt.Printf("Upload failed: %v\n", err)
} else {
fmt.Println("Upload completed.")
}
}
func main() {
log.SetFlags(0)
flag.Usage = usage
flag.Parse()
args := flag.Args()
if flag.NArg() == 0 {
flag.Usage()
os.Exit(2)
}
if args[0] == "help" {
flag.CommandLine.SetOutput(os.Stdout)
switch len(args) {
case 1:
flag.Usage()
case 2:
help(args[1])
default:
flag.Usage()
failf("too many arguments to \"help\"")
}
os.Exit(0)
}
cmd := findCommand(args[0])
if cmd == nil {
flag.Usage()
os.Exit(2)
}
cmd.flags.Parse(args[1:]) // will exit on error
args = cmd.flags.Args()
if !cmd.hasArgs && len(args) > 0 {
help(cmd.name())
failf("command %s does not accept any arguments", cmd.name())
}
cmd.run(args)
}
@@ -0,0 +1,146 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package counter
// The implementation of this package and tests are located in
// internal/counter, which can be shared with the upload package.
// TODO(hyangah): use of type aliases prevents nice documentation
// rendering in go doc or pkgsite. Fix this either by avoiding
// type aliasing or restructuring the internal/counter package.
import (
"flag"
"path"
"runtime/debug"
"golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
)
// Inc increments the counter with the given name.
func Inc(name string) {
New(name).Inc()
}
// Add adds n to the counter with the given name.
func Add(name string, n int64) {
New(name).Add(n)
}
// New returns a counter with the given name.
// New can be called in global initializers and will be compiled down to
// linker-initialized data. That is, calling New to initialize a global
// has no cost at program startup.
//
// See "Counter Naming" in the package doc for a description of counter naming
// conventions.
func New(name string) *Counter {
// Note: not calling DefaultFile.New in order to keep this
// function something the compiler can inline and convert
// into static data initializations, with no init-time footprint.
// TODO(hyangah): is it trivial enough for the compiler to inline?
return counter.New(name)
}
// A Counter is a single named event counter.
// A Counter is safe for use by multiple goroutines simultaneously.
//
// Counters should typically be created using New
// and stored as global variables, like:
//
// package mypackage
// var errorCount = counter.New("mypackage/errors")
//
// (The initialization of errorCount in this example is handled
// entirely by the compiler and linker; this line executes no code
// at program startup.)
//
// Then code can call Add to increment the counter
// each time the corresponding event is observed.
//
// Although it is possible to use New to create
// a Counter each time a particular event needs to be recorded,
// that usage fails to amortize the construction cost over
// multiple calls to Add, so it is more expensive and not recommended.
type Counter = counter.Counter
// A StackCounter is the in-memory knowledge about a stack counter.
// StackCounters are more expensive to use than regular Counters,
// requiring, at a minimum, a call to runtime.Callers.
type StackCounter = counter.StackCounter
// NewStack returns a new stack counter with the given name and depth.
//
// See "Counter Naming" in the package doc for a description of counter naming
// conventions.
func NewStack(name string, depth int) *StackCounter {
return counter.NewStack(name, depth)
}
// Open prepares telemetry counters for recording to the file system.
//
// If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
// counter file on disk and starts to mmap telemetry counters to the file.
// Open also persists any counters already created in the current process.
//
// Open should only be called from short-lived processes such as command line
// tools. If your process is long-running, use [OpenAndRotate].
func Open() {
counter.Open(false)
}
// OpenAndRotate is like [Open], but also schedules a rotation of the counter
// file when it expires.
//
// See golang/go#68497 for background on why [OpenAndRotate] is a separate API.
//
// TODO(rfindley): refactor Open and OpenAndRotate for Go 1.24.
func OpenAndRotate() {
counter.Open(true)
}
// OpenDir prepares telemetry counters for recording to the file system, using
// the specified telemetry directory, if it is not the empty string.
//
// If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
// counter file on disk and starts to mmap telemetry counters to the file.
// Open also persists any counters already created in the current process.
func OpenDir(telemetryDir string) {
if telemetryDir != "" {
telemetry.Default = telemetry.NewDir(telemetryDir)
}
counter.Open(false)
}
// CountFlags creates a counter for every flag that is set
// and increments the counter. The name of the counter is
// the concatenation of prefix and the flag name.
//
// For instance, CountFlags("gopls/flag:", *flag.CommandLine)
func CountFlags(prefix string, fs flag.FlagSet) {
fs.Visit(func(f *flag.Flag) {
New(prefix + f.Name).Inc()
})
}
// CountCommandLineFlags creates a counter for every flag
// that is set in the default flag.CommandLine FlagSet using
// the counter name binaryName+"/flag:"+flagName where
// binaryName is the base name of the Path embedded in the
// binary's build info. If the binary does not have embedded build
// info, the "flag:"+flagName counter will be incremented.
//
// CountCommandLineFlags must be called after flags are parsed
// with flag.Parse.
//
// For instance, if the -S flag is passed to cmd/compile and
// CountCommandLineFlags is called after flags are parsed,
// the "compile/flag:S" counter will be incremented.
func CountCommandLineFlags() {
prefix := "flag:"
if buildInfo, ok := debug.ReadBuildInfo(); ok && buildInfo.Path != "" {
prefix = path.Base(buildInfo.Path) + "/" + prefix
}
CountFlags(prefix, *flag.CommandLine)
}
@@ -0,0 +1,44 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package counter_test
import (
"os"
"os/exec"
"testing"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/testenv"
)
const telemetryDirEnvVar = "_COUNTER_TEST_TELEMETRY_DIR"
func TestMain(m *testing.M) {
if dir := os.Getenv(telemetryDirEnvVar); dir != "" {
// Run for TestOpenAPIMisuse.
telemetry.Default = telemetry.NewDir(dir)
counter.Open()
counter.OpenAndRotate() // should panic
os.Exit(0)
}
os.Exit(m.Run())
}
func TestOpenAPIMisuse(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
// Test that Open and OpenAndRotate cannot be used simultaneously.
exe, err := os.Executable()
if err != nil {
t.Fatal(err)
}
cmd := exec.Command(exe)
cmd.Env = append(os.Environ(), telemetryDirEnvVar+"="+t.TempDir())
if err := cmd.Run(); err == nil {
t.Error("Failed to detect API misuse: no error from calling both Open and OpenAndRotate")
}
}
@@ -0,0 +1,63 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// countertest provides testing utilities for counters.
// This package cannot be used except for testing.
package countertest
import (
"sync"
"golang.org/x/telemetry/counter"
ic "golang.org/x/telemetry/internal/counter"
"golang.org/x/telemetry/internal/telemetry"
)
var (
openedMu sync.Mutex
opened bool
)
// SupportedPlatform reports if this platform supports Open()
const SupportedPlatform = !telemetry.DisabledOnPlatform
func isOpen() bool {
openedMu.Lock()
defer openedMu.Unlock()
return opened
}
// Open enables telemetry data writing to disk.
// This is supposed to be called once during the program execution
// (i.e. typically in TestMain), and must not be used with
// golang.org/x/telemetry/counter.Open.
func Open(telemetryDir string) {
openedMu.Lock()
defer openedMu.Unlock()
if opened {
panic("Open was called more than once")
}
telemetry.Default = telemetry.NewDir(telemetryDir)
// TODO(rfindley): reinstate test coverage with counter rotation enabled.
// Before the [counter.Open] and [counter.OpenAndRotate] APIs were split,
// this called counter.Open (which rotated!).
counter.Open()
opened = true
}
// ReadCounter reads the given counter.
func ReadCounter(c *counter.Counter) (count uint64, _ error) {
return ic.Read(c)
}
// ReadStackCounter reads the given StackCounter.
func ReadStackCounter(c *counter.StackCounter) (stackCounts map[string]uint64, _ error) {
return ic.ReadStack(c)
}
// ReadFile reads the counters and stack counters from the given file.
func ReadFile(name string) (counters, stackCounters map[string]uint64, _ error) {
return ic.ReadFile(name)
}
@@ -0,0 +1,16 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.21
package countertest
import "testing"
func init() {
// Extra safety check for go1.21+.
if !testing.Testing() {
panic("use of this package is disallowed in non-testing code")
}
}
@@ -0,0 +1,90 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.21
package countertest
import (
"fmt"
"slices"
"strings"
"sync"
"testing"
"golang.org/x/telemetry/counter"
"golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/testenv"
)
func TestReadCounter(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
c := counter.New("foobar")
got, err := ReadCounter(c)
if err != nil {
t.Errorf("ReadCounter = (%d, %v), want (0,nil)", got, err)
}
if got != 0 {
t.Fatalf("ReadCounter = %d, want 0", got)
}
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
if got, err := ReadCounter(c); err != nil || got != 100 {
t.Errorf("ReadCounter = (%v, %v), want (%v, nil)", got, err, 100)
}
}
func TestReadStackCounter(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
c := counter.NewStack("foobar", 8)
if got, err := ReadStackCounter(c); err != nil || len(got) != 0 {
t.Errorf("ReadStackCounter = (%q, %v), want (%v, nil)", got, err, 0)
}
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
c.Inc() // one stack!
wg.Done()
}()
}
wg.Wait()
got, err := ReadStackCounter(c)
if err != nil || len(got) != 1 {
t.Fatalf("ReadStackCounter = (%v, %v), want to read one entry", stringify(got), err)
}
for k, v := range got {
if !strings.Contains(k, t.Name()) || v != 100 {
t.Fatalf("ReadStackCounter = %v, want a stack counter with value 100", got)
}
}
}
func TestSupport(t *testing.T) {
if SupportedPlatform == telemetry.DisabledOnPlatform {
t.Errorf("supported mismatch: us %v, telemetry.internal.Disabled %v",
SupportedPlatform, telemetry.DisabledOnPlatform)
}
}
func stringify(m map[string]uint64) string {
kv := make([]string, 0, len(m))
for k, v := range m {
kv = append(kv, fmt.Sprintf("%q:%v", k, v))
}
slices.Sort(kv)
return "{" + strings.Join(kv, " ") + "}"
}
@@ -0,0 +1,58 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package counter implements a simple counter system for collecting
// totally public telemetry data.
//
// There are two kinds of counters, basic counters and stack counters.
// Basic counters are created by [New].
// Stack counters are created by [NewStack].
// Both are incremented by calling Inc().
//
// Basic counters are very cheap. Stack counters are more expensive, as they
// require parsing the stack. (Stack counters are implemented as basic counters
// whose names are the concatenation of the name and the stack trace. There is
// an upper limit on the size of this name, about 4K bytes. If the name is too
// long the stack will be truncated and "truncated" appended.)
//
// When counter files expire they are turned into reports by the upload
// package. The first time any counter file is created for a user, a random day
// of the week is selected on which counter files will expire. For the first
// week, that day is more than 7 days (but not more than two weeks) in the
// future. After that the counter files expire weekly on the same day of the
// week.
//
// # Counter Naming
//
// Counter names passed to [New] and [NewStack] should follow these
// conventions:
//
// - Names cannot contain whitespace or newlines.
//
// - Names must be valid unicode, with no unprintable characters.
//
// - Names may contain at most one ':'. In the counter "foo:bar", we refer to
// "foo" as the "chart name" and "bar" as the "bucket name".
//
// - The '/' character should partition counter names into a hierarchy. The
// root of this hierarchy should identify the logical entity that "owns"
// the counter. This could be an application, such as "gopls" in the case
// of "gopls/client:vscode", or a shared library, such as "crash" in the
// case of the "crash/crash" counter owned by the crashmonitor library. If
// the entity name itself contains a '/', that's ok: "cmd/go/flag" is fine.
//
// - Words should be '-' separated, as in "gopls/completion/errors-latency"
//
// - Histograms should use bucket names identifying upper bounds with '<'.
// For example given two counters "gopls/completion/latency:<50ms" and
// "gopls/completion/latency:<100ms", the "<100ms" bucket counts events
// with latency in the half-open interval [50ms, 100ms).
//
// # Debugging
//
// The GODEBUG environment variable can enable printing of additional debug
// information for counters. Adding GODEBUG=countertrace=1 to the environment
// of a process using counters causes the x/telemetry/counter package to log
// counter information to stderr.
package counter
@@ -0,0 +1,12 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package crashmonitor
import ic "golang.org/x/telemetry/internal/crashmonitor"
// Supported reports whether the runtime supports [runtime.SetCrashOutput].
//
// TODO(adonovan): eliminate once go1.23+ is assured.
func Supported() bool { return ic.Supported() }
@@ -0,0 +1,12 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
import "golang.org/x/telemetry/internal/telemetry"
// Dir returns the telemetry directory.
func Dir() string {
return telemetry.Default.Dir()
}
@@ -0,0 +1 @@
package telemetry
@@ -0,0 +1,10 @@
module golang.org/x/telemetry
go 1.20
require golang.org/x/mod v0.20.0
require (
golang.org/x/sync v0.8.0
golang.org/x/sys v0.23.0
)
@@ -0,0 +1,6 @@
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
@@ -0,0 +1,85 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The chartconfig package defines the ChartConfig type, representing telemetry
// chart configuration, as well as utilities for parsing and validating this
// configuration.
//
// Chart configuration defines the set of aggregations active on the telemetry
// server, and are used to derive which data needs to be uploaded by users.
// See the original blog post for more details:
//
// https://research.swtch.com/telemetry-design#configuration
//
// The record format defined in this package differs slightly from that of the
// blog post. This format is still experimental, and subject to change.
//
// Configuration records consist of fields, comments, and whitespace. A field
// is defined by a line starting with a valid key, followed immediately by ":",
// and then a textual value, which cannot include the comment separator '#'.
//
// Comments start with '#', and extend to the end of the line.
//
// The following keys are supported. Any entry not marked as (optional) must be
// provided.
//
// - title: the chart title.
// - description: (optional) a longer description of the chart.
// - issue: a go issue tracker URL proposing the chart configuration.
// Multiple issues may be provided by including additional 'issue:' lines.
// All proposals must be in the 'accepted' state.
// - type: the chart type: currently only partition, histogram, and stack are
// supported.
// - program: the package path of the program for which this chart applies.
// - version: (optional) the first version for which this chart applies. Must
// be a valid semver value.
// - counter: the primary counter this chart illustrates, including buckets
// for histogram and partition charts.
// - depth: (optional) stack counters only; the maximum stack depth to collect
// - error: (optional) the desired error rate for this chart, which
// determines collection rate
//
// Multiple records are separated by "---" lines.
//
// For example:
//
// # This config defines an ordinary counter.
// counter: gopls/editor:{emacs,vim,vscode,other} # TODO(golang/go#34567): add more editors
// title: Editor Distribution
// description: measure editor distribution for gopls users.
// type: partition
// issue: https://go.dev/issue/12345
// program: golang.org/x/tools/gopls
// version: v1.0.0
// version: [v2.0.0, v2.3.4]
// version: [v3.0.0, ]
//
// ---
//
// # This config defines a stack counter.
// counter: gopls/bug
// title: Gopls bug reports.
// description: Stacks of bugs encountered on the gopls server.
// issue: https://go.dev/12345
// issue: https://go.dev/23456 # increase stack depth
// type: stack
// program: golang.org/x/tools/gopls
// depth: 10
package chartconfig
// A ChartConfig defines the configuration for a single chart/collection on the
// telemetry server.
//
// See the package documentation for field definitions.
type ChartConfig struct {
Title string
Description string
Issue []string
Type string
Program string
Counter string
Depth int
Error float64 // TODO(rfindley) is Error still useful?
Version string
}
@@ -0,0 +1,148 @@
# Note: these are approved chart configs, used to generate the upload config.
# For the chart config file format, see chartconfig.go.
title: Editor Distribution
counter: gopls/client:{vscode,vscodium,vscode-insiders,code-server,eglot,govim,neovim,coc.nvim,sublimetext,other}
description: measure editor distribution for gopls users.
type: partition
issue: https://go.dev/issue/61038
issue: https://go.dev/issue/62214 # add vscode-insiders
program: golang.org/x/tools/gopls
version: v0.13.0 # temporarily back-version to demonstrate config generation.
---
title: Go versions in use for gopls views
counter: gopls/goversion:{1.16,1.17,1.18,1.19,1.20,1.21,1.22,1.23,1.24,1.25,1.26,1.27,1.28,1.29,1.30}
description: measure go version usage distribution.
type: partition
issue: https://go.dev/issue/62248
program: golang.org/x/tools/gopls
version: v0.13.0
---
title: Number of bug report calls
counter: gopls/bug
description: count the bugs reported through gopls/internal/bug APIs.
type: stack
issue: https://go.dev/issue/62249
program: golang.org/x/tools/gopls
depth: 16
version: v0.13.0
---
counter: crash/crash
title: Unexpected Go crashes
description: stacks of goroutines running when the Go program crashed
type: stack
issue: https://go.dev/issue/65696
program: golang.org/x/tools/gopls
depth: 16
version: v0.15.0
---
counter: crash/malformed
title: Failure to parse runtime crash output
description: count of runtime crash messages that failed to parse
type: partition
issue: https://go.dev/issue/65696
program: golang.org/x/tools/gopls
version: v0.15.0
---
counter: crash/no-running-goroutine
title: Failure to identify any running goroutine in the crash output
description: count of runtime crash messages that don't have a running goroutine (e.g. deadlock)
type: partition
issue: https://go.dev/issue/65696
program: golang.org/x/tools/gopls
version: v0.15.0
---
counter: go/invocations
title: cmd/go invocations
description: Number of invocations of the go command
type: partition
issue: https://go.dev/issue/67244
program: cmd/go
version: go1.23rc1
---
counter: go/build/flag:{
buildmode
}
title: cmd/go flags
description: Flag names of flags provided to the go command
type: partition
issue: https://go.dev/issue/67244
program: cmd/go
version: go1.23rc1
---
counter: go/build/flag/buildmode:{
archive,
c-archive,
c-shared,
default,
exe,
pie,
shared,
plugin
}
title: cmd/go buildmode values
description: Buildmode values for the go command
type: partition
issue: https://go.dev/issue/67244
program: cmd/go
version: go1.23rc1
---
counter: compile/invocations
title: cmd/compile invocations
description: Number of invocations of the go compiler
type: partition
issue: https://go.dev/issue/67244
program: cmd/compile
version: go1.23rc1
---
title: Compiler bug report calls
counter: compile/bug
description: count stacks for cases where cmd/compile has a fatal error
type: stack
issue: https://go.dev/issue/67244
program: cmd/compile
depth: 16
version: go1.23rc1
---
counter: govulncheck/scan:{symbol,package,module}
title: Scan Level Distribution
description: measure govulncheck scan level distribution
type: partition
issue: https://go.dev/issue/67678
program: golang.org/x/vuln/cmd/govulncheck
---
counter: govulncheck/mode:{source,binary,extract,query,convert}
title: Scan Mode Distribution
description: measure govulncheck scan mode distribution
type: partition
issue: https://go.dev/issue/67678
program: golang.org/x/vuln/cmd/govulncheck
---
counter: govulncheck/format:{text,json,sarif,openvex}
title: Output Format Distribution
description: measure govulncheck output format distribution
type: partition
issue: https://go.dev/issue/67678
program: golang.org/x/vuln/cmd/govulncheck
---
counter: govulncheck/show:{none,traces,color,verbose,version}
title: Show Options Distribution
description: measure govulncheck show flag distribution
type: partition
issue: https://go.dev/issue/67678
program: golang.org/x/vuln/cmd/govulncheck
---
counter: govulncheck/assumptions:{multi-patterns,no-binary-platform,no-relative-path,no-go-root,local-replace,unknown-pkg-mod-path}
title: Code Invariants Distribution
description: measure distribution of failed govulncheck internal assumptions
type: partition
issue: https://go.dev/issue/67678
program: golang.org/x/vuln/cmd/govulncheck
---
counter: gopls/gotoolchain:{auto,path,local,other}
title: GOTOOLCHAIN types used with gopls
description: measure the types of GOTOOLCHAIN values used with gopls
type: partition
issue: https://go.dev/issue/68771
program: golang.org/x/tools/gopls
version: v0.16.0
@@ -0,0 +1,219 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package chartconfig
import (
_ "embed"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"unicode"
)
//go:embed config.txt
var chartConfig []byte
func Raw() []byte {
return chartConfig
}
// Load loads and parses the current chart config.
func Load() ([]ChartConfig, error) {
return Parse(chartConfig)
}
// Parse parses ChartConfig records from the provided raw data, returning an
// error if the config has invalid syntax. See the package documentation for a
// description of the record syntax.
//
// Even with correct syntax, the resulting chart config may not meet all the
// requirements described in the package doc. Call [Validate] to check whether
// the config data is coherent.
func Parse(data []byte) ([]ChartConfig, error) {
// Collect field information for the record type.
var (
prefixes []string // for parse errors
fields = make(map[string]reflect.StructField) // key -> struct field
)
{
typ := reflect.TypeOf(ChartConfig{})
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
key := strings.ToLower(f.Name)
if _, ok := fieldParsers[key]; !ok {
panic(fmt.Sprintf("no parser for field %q", f.Name))
}
prefixes = append(prefixes, "'"+key+":'")
fields[key] = f
}
sort.Strings(prefixes)
}
// Read records, separated by '---'
var (
records []ChartConfig
inProgress = new(ChartConfig) // record value currently being parsed
set = make(map[string]bool) // fields that are set so far; empty records are skipped
)
flushRecord := func() {
if len(set) > 0 { // only flush non-empty records
records = append(records, *inProgress)
}
inProgress = new(ChartConfig)
set = make(map[string]bool)
}
// Within bucket braces in counter fields, newlines are ignored.
// if we're in the middle of a multiline counter field, accumulatedCounterText
// contains the joined lines of the field up to the current line. Once
// a line containing an end brace is reached, line will be set to the
// joined lines of accumulatedCounterText and processed as a single line.
var accumulatedCounterText string
for lineNum, line := range strings.Split(string(data), "\n") {
if line == "---" {
if accumulatedCounterText != "" {
return nil, fmt.Errorf("line %d: reached end of record while processing multiline counter field", lineNum)
}
flushRecord()
continue
}
text, _, _ := strings.Cut(line, "#") // trim comments
// Processing of counter fields which can appear across multiple lines.
// See comment on accumulatedCounterText.
if accumulatedCounterText == "" {
if oi := strings.Index(text, "{"); oi >= 0 {
if strings.Contains(text[:oi], "}") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
}
if strings.Contains(text[oi+len("{"):], "{") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '{'", lineNum, line)
}
if !strings.HasPrefix(text, "counter:") {
return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear within a counter field", lineNum, line)
}
accumulatedCounterText = strings.TrimRightFunc(text, unicode.IsSpace)
// Don't continue here. If the counter field is a single line
// the check for the close brace below will close the line
// and process it as text. Set text to "" so when it's appended to
// accumulatedCounterText we don't add the line twice.
text = ""
} else if strings.Contains(text, "}") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
}
}
if accumulatedCounterText != "" {
if strings.Contains(text, "{") {
return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear once within a counter field", lineNum, line)
}
accumulatedCounterText += strings.TrimSpace(text)
if ci := strings.Index(accumulatedCounterText, "}"); ci >= 0 {
if strings.Contains(accumulatedCounterText[ci+len("}"):], "}") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
}
if ci > 0 && strings.HasSuffix(accumulatedCounterText[:ci], ",") {
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}' after ','", lineNum, line)
}
text = accumulatedCounterText
accumulatedCounterText = ""
} else {
// We're in the middle of a multiline counter field. Continue
// processing.
continue
}
}
var key string
for k := range fields {
prefix := k + ":"
if strings.HasPrefix(text, prefix) {
key = k
text = text[len(prefix):]
break
}
}
text = strings.TrimSpace(text)
if text == "" {
// Check for empty lines before the field == nil check below.
// Lines consisting only of whitespace and comments are OK.
continue
}
if key == "" {
return nil, fmt.Errorf("line %d: invalid line %q: lines must be '---', consist only of whitespace/comments, or start with %s", lineNum, line, strings.Join(prefixes, ", "))
}
field := fields[key]
v := reflect.ValueOf(inProgress).Elem().FieldByName(field.Name)
if set[key] && field.Type.Kind() != reflect.Slice {
return nil, fmt.Errorf("line %d: field %s may not be repeated", lineNum, strings.ToLower(field.Name))
}
parser := fieldParsers[key]
if err := parser(v, text); err != nil {
return nil, fmt.Errorf("line %d: field %q: %v", lineNum, field.Name, err)
}
set[key] = true
}
if accumulatedCounterText != "" {
return nil, fmt.Errorf("reached end of file while processing multiline counter field")
}
flushRecord()
return records, nil
}
// A fieldParser parses the provided input and writes to v, which must be
// addressable.
type fieldParser func(v reflect.Value, input string) error
var fieldParsers = map[string]fieldParser{
"title": parseString,
"description": parseString,
"issue": parseSlice(parseString),
"type": parseString,
"program": parseString,
"counter": parseString,
"depth": parseInt,
"error": parseFloat,
"version": parseString,
}
func parseString(v reflect.Value, input string) error {
v.SetString(input)
return nil
}
func parseInt(v reflect.Value, input string) error {
i, err := strconv.ParseInt(input, 10, 64)
if err != nil {
return fmt.Errorf("invalid int value %q", input)
}
v.SetInt(i)
return nil
}
func parseFloat(v reflect.Value, input string) error {
f, err := strconv.ParseFloat(input, 64)
if err != nil {
return fmt.Errorf("invalid float value %q", input)
}
v.SetFloat(f)
return nil
}
func parseSlice(elemParser fieldParser) fieldParser {
return func(v reflect.Value, input string) error {
elem := reflect.New(v.Type().Elem()).Elem()
v.Set(reflect.Append(v, elem))
elem = v.Index(v.Len() - 1)
if err := elemParser(elem, input); err != nil {
return err
}
return nil
}
}
@@ -0,0 +1,268 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package chartconfig_test
import (
"reflect"
"testing"
"golang.org/x/telemetry/internal/chartconfig"
)
func TestLoad(t *testing.T) {
// Test that we can actually load the chart config.
if _, err := chartconfig.Load(); err != nil {
t.Errorf("Load() failed: %v", err)
}
}
func TestParse(t *testing.T) {
tests := []struct {
name string
input string
want []chartconfig.ChartConfig
}{
{"empty", "", nil},
{"single field", "title: A", []chartconfig.ChartConfig{{Title: "A"}}},
{
"basic", `
title: A
description: B
type: C
program: D
counter: E
issue: F1
issue: F2
depth: 2
error: 0.1
version: v2.0.0
`,
[]chartconfig.ChartConfig{{
Title: "A",
Description: "B",
Type: "C",
Program: "D",
Counter: "E",
Issue: []string{"F1", "F2"},
Depth: 2,
Error: 0.1,
Version: "v2.0.0",
}},
},
{
"partial", `
title: A
description: B
`,
[]chartconfig.ChartConfig{
{Title: "A", Description: "B"},
},
},
{
"comments and whitespace", `
# A comment
title: A # a line comment
# Another comment
description: B
`,
[]chartconfig.ChartConfig{
{Title: "A", Description: "B"},
},
},
{
"multi-record", `
# Empty records are skipped
---
title: A
description: B
---
title: C
description: D
`,
[]chartconfig.ChartConfig{
{Title: "A", Description: "B"},
{Title: "C", Description: "D"},
},
},
{
"example", `
title: Editor Distribution
counter: gopls/editor:{emacs,vim,vscode,other}
description: measure editor distribution for gopls users.
type: partition
issue: TBD
program: golang.org/x/tools/gopls
`,
[]chartconfig.ChartConfig{
{
Title: "Editor Distribution",
Description: "measure editor distribution for gopls users.",
Counter: "gopls/editor:{emacs,vim,vscode,other}",
Type: "partition",
Issue: []string{"TBD"},
Program: "golang.org/x/tools/gopls",
},
},
},
{
"multiline counter field", `
counter: foo:{
bar,
baz
}
`,
[]chartconfig.ChartConfig{
{Counter: "foo:{bar,baz}"},
},
},
{
"multiline counter field with braces immediately next to text", `
counter: foo:{bar,
baz}
`,
[]chartconfig.ChartConfig{
{Counter: "foo:{bar,baz}"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := chartconfig.Parse([]byte(test.input))
if err != nil {
t.Fatalf("Parse(...) failed: %v", err)
}
if len(got) != len(test.want) {
t.Fatalf("Parse(...) returned %d records, want %d", len(got), len(test.want))
}
for i, got := range got {
want := test.want[i]
if !reflect.DeepEqual(got, want) {
t.Errorf("Parse(...): record %d = %#v, want %#v", i, got, want)
}
}
})
}
}
func TestParseErrors(t *testing.T) {
tests := []struct {
name string
input string
}{
{
"leading space",
`
title: foo
`,
},
{
"unknown key",
`
foo: bar
`,
},
{
"bad separator",
`
title: foo
--- # comments aren't allowed after separators
title: bar
`,
},
{
"invalid depth",
`
depth: notanint
`,
},
{
"open curly brace not in counter field",
`
title: {
`,
},
{
"close curly brace not in counter field",
`
title: }
`,
},
{
"end of record within multiline counter field",
`
counter: foo{
bar
---
title: baz
`,
},
{
"end of file within multiline counter field",
`
counter: foo{
bar
`,
},
{
"close curly before open curly",
`
counter: }foo{
bar
}`,
},
{
"open curly after close curly",
`
counter: foo{
bar
} {`,
},
{
"open curly after open curly same line",
`
counter: foo{{
bar
}`,
},
{
"open curly after open curly different line",
`
counter: foo{
{bar
}`,
},
{
"close curly after close curly",
`
counter: foo{
bar
} }`,
},
{
"comma right before close curly",
`
counter: foo{
bar,
baz,
} }`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := chartconfig.Parse([]byte(test.input))
if err == nil {
t.Fatalf("Parse(...) succeeded unexpectedly")
}
})
}
}
@@ -0,0 +1,140 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// package config provides methods for loading and querying a
// telemetry upload config file.
package config
import (
"encoding/json"
"os"
"strings"
"golang.org/x/telemetry/internal/telemetry"
)
// Config is a wrapper around telemetry.UploadConfig that provides some
// convenience methods for checking the contents of a report.
type Config struct {
*telemetry.UploadConfig
program map[string]bool
goos map[string]bool
goarch map[string]bool
goversion map[string]bool
pgversion map[pgkey]bool
pgcounter map[pgkey]bool
pgcounterprefix map[pgkey]bool
pgstack map[pgkey]bool
rate map[pgkey]float64
}
type pgkey struct {
program, key string
}
func ReadConfig(file string) (*Config, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var cfg telemetry.UploadConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
return NewConfig(&cfg), nil
}
func NewConfig(cfg *telemetry.UploadConfig) *Config {
ucfg := Config{UploadConfig: cfg}
ucfg.goos = set(ucfg.GOOS)
ucfg.goarch = set(ucfg.GOARCH)
ucfg.goversion = set(ucfg.GoVersion)
ucfg.program = make(map[string]bool, len(ucfg.Programs))
ucfg.pgversion = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.pgcounter = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.pgcounterprefix = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.pgstack = make(map[pgkey]bool, len(ucfg.Programs))
ucfg.rate = make(map[pgkey]float64)
for _, p := range ucfg.Programs {
ucfg.program[p.Name] = true
for _, v := range p.Versions {
ucfg.pgversion[pgkey{p.Name, v}] = true
}
for _, c := range p.Counters {
for _, e := range Expand(c.Name) {
ucfg.pgcounter[pgkey{p.Name, e}] = true
ucfg.rate[pgkey{p.Name, e}] = c.Rate
}
prefix, _, found := strings.Cut(c.Name, ":")
if found {
ucfg.pgcounterprefix[pgkey{p.Name, prefix}] = true
}
}
for _, s := range p.Stacks {
ucfg.pgstack[pgkey{p.Name, s.Name}] = true
ucfg.rate[pgkey{p.Name, s.Name}] = s.Rate
}
}
return &ucfg
}
func (r *Config) HasProgram(s string) bool {
return r.program[s]
}
func (r *Config) HasGOOS(s string) bool {
return r.goos[s]
}
func (r *Config) HasGOARCH(s string) bool {
return r.goarch[s]
}
func (r *Config) HasGoVersion(s string) bool {
return r.goversion[s]
}
func (r *Config) HasVersion(program, version string) bool {
return r.pgversion[pgkey{program, version}]
}
func (r *Config) HasCounter(program, counter string) bool {
return r.pgcounter[pgkey{program, counter}]
}
func (r *Config) HasCounterPrefix(program, prefix string) bool {
return r.pgcounterprefix[pgkey{program, prefix}]
}
func (r *Config) HasStack(program, stack string) bool {
return r.pgstack[pgkey{program, stack}]
}
func (r *Config) Rate(program, name string) float64 {
return r.rate[pgkey{program, name}]
}
func set(slice []string) map[string]bool {
s := make(map[string]bool, len(slice))
for _, v := range slice {
s[v] = true
}
return s
}
// Expand takes a counter defined with buckets and expands it into distinct
// strings for each bucket
func Expand(counter string) []string {
prefix, rest, hasBuckets := strings.Cut(counter, "{")
var counters []string
if hasBuckets {
buckets := strings.Split(strings.TrimSuffix(rest, "}"), ",")
for _, b := range buckets {
counters = append(counters, prefix+b)
}
} else {
counters = append(counters, prefix)
}
return counters
}
@@ -0,0 +1,97 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package config
import (
_ "embed"
"encoding/json"
"os"
"path/filepath"
"testing"
"golang.org/x/telemetry/internal/telemetry"
)
func TestConfig(t *testing.T) {
f, err := os.Open(filepath.FromSlash("../../config/config.json"))
if os.IsNotExist(err) {
t.Skip("config file not found")
}
if err != nil {
t.Fatal(err)
}
defer f.Close()
var cfg telemetry.UploadConfig
d := json.NewDecoder(f)
d.DisallowUnknownFields()
if err := d.Decode(&cfg); err != nil {
t.Fatal(err)
}
}
func TestInternalConfig(t *testing.T) {
got, err := ReadConfig("testdata/config.json")
if err != nil {
t.Fatal(err)
}
wantGOOS := []string{"linux", "darwin"}
wantGOARCH := []string{"amd64", "arm64"}
wantGoVersion := []string{"go1.20", "go1.20.1"}
wantPrograms := []string{"golang.org/x/tools/gopls", "cmd/go"}
wantVersions := [][2]string{
{"golang.org/x/tools/gopls", "v0.10.1"},
{"golang.org/x/tools/gopls", "v0.11.0"},
}
wantCounters := [][2]string{
{"golang.org/x/tools/gopls", "editor:emacs"},
{"golang.org/x/tools/gopls", "editor:vim"},
{"golang.org/x/tools/gopls", "editor:vscode"},
{"golang.org/x/tools/gopls", "editor:other"},
{"cmd/go", "go/buildcache/miss:0"},
{"cmd/go", "go/buildcache/miss:1"},
{"cmd/go", "go/buildcache/miss:10"},
{"cmd/go", "go/buildcache/miss:100"},
}
wantPrefix := [][2]string{
{"golang.org/x/tools/gopls", "editor"},
{"cmd/go", "go/buildcache/miss"},
}
for _, w := range wantGOOS {
if !got.HasGOOS(w) {
t.Errorf("got.HasGOOS(%s) = false: want true", w)
}
}
for _, w := range wantGOARCH {
if !got.HasGOARCH(w) {
t.Errorf("got.HasGOARCH(%s) = false: want true", w)
}
}
for _, w := range wantGoVersion {
if !got.HasGoVersion(w) {
t.Errorf("got.HasGoVersion(%s) = false: want true", w)
}
}
for _, w := range wantPrograms {
if !got.HasProgram(w) {
t.Errorf("got.HasProgram(%s) = false: want true", w)
}
}
for _, w := range wantVersions {
if !got.HasVersion(w[0], w[1]) {
t.Errorf("got.HasVersion(%s, %s) = false: want true", w[0], w)
}
}
for _, w := range wantCounters {
if !got.HasCounter(w[0], w[1]) {
t.Errorf("got.HasCounter(%s, %s) = false: want true", w[0], w[1])
}
}
for _, w := range wantPrefix {
if !got.HasCounterPrefix(w[0], w[1]) {
t.Errorf("got.HasCounterPrefix(%s, %s) = false: want true", w[0], w[1])
}
}
}
@@ -0,0 +1,43 @@
{
"Version": "v0.0.1-test",
"GOOS": [
"linux",
"darwin"
],
"GOARCH": [
"amd64",
"arm64"
],
"GoVersion": [
"go1.20",
"go1.20.1"
],
"Programs": [
{
"Name": "golang.org/x/tools/gopls",
"Versions": [
"v0.10.1",
"v0.11.0"
],
"Counters": [
{
"Name": "editor:{emacs,vim,vscode,other}",
"Rate": 0.01
}
]
},
{
"Name": "cmd/go",
"Versions": [
"v0.10.1",
"v0.11.0"
],
"Counters": [
{
"Name": "go/buildcache/miss:{0,0.1,0.2,0.5,1,10,100,2,20,5,50}",
"Rate": 0.01
}
]
}
]
}
@@ -0,0 +1,536 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run . -w
//go:build go1.22
// Package configgen generates the upload config file stored in the config.json
// file of golang.org/x/telemetry/config based on the chartconfig stored in
// config.txt.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"go/version"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"golang.org/x/mod/semver"
"golang.org/x/telemetry/internal/chartconfig"
"golang.org/x/telemetry/internal/telemetry"
)
var (
write = flag.Bool("w", false, "if set, write the config file; otherwise, print to stdout")
force = flag.Bool("f", false, "if set, force the write of the config file even if the current content is still valid")
// SamplingRate is the fraction of otherwise uploadable reports that will be uploaded
SamplingRate = 1.0
)
func main() {
flag.Parse()
gcfgs, err := chartconfig.Load()
if err != nil {
log.Fatal(err)
}
// The padding heuristics below are based on the example of gopls.
//
// The goal is to pad enough versions for a quarter.
uCfg, err := generate(gcfgs, padding{
// 6 releases into the future translates to approximately three months for gopls.
releases: 6,
// We may release gopls 1.0, but won't release 2.0 in a three month timespan!
maj: 1,
// We don't usually do more than one minor release a month.
majmin: 3,
// Since golang/go#55267, which committed to adhering to semver, gopls
// hasn't had more than 5 patches per minor version.
patch: 6,
// Gopls has never had more than 4 prereleases.
pre: 4,
})
if err != nil {
log.Fatal(err)
}
cfgJSON, err := json.MarshalIndent(uCfg, "", "\t")
if err != nil {
log.Fatal(err)
}
if !*write {
fmt.Println(string(cfgJSON))
os.Exit(0)
}
configFile, err := configFile()
if err != nil {
log.Fatalf("finding config file: %v", err)
}
if !*force {
currentCfg, err := readConfig(configFile)
if err != nil {
log.Fatal(err)
}
// Guarantee that we have enough padding to do two patches releases tomorrow.
minCfg, err := generate(gcfgs, padding{
releases: 2,
maj: 1,
majmin: 1, // we're not ever going to do more than one major/minor release in a day
patch: 2,
pre: 2, // in a single day, we wouldn't prep more than two prereleases per version
})
if err != nil {
log.Fatal(err)
}
if contains(currentCfg, minCfg) {
fmt.Fprintln(os.Stderr, "not writing the config file as it is still valid; use -f to force")
os.Exit(0)
}
}
if err := os.WriteFile(configFile, cfgJSON, 0666); err != nil {
log.Fatal(err)
}
}
// configFile returns the path to the x/telemetry/config config.json file in
// this repo.
//
// The file must already exist: this won't be a valid location if running from
// the module cache; this functionality only works when executed from the
// telemetry repo.
func configFile() (string, error) {
out, err := exec.Command("go", "list", "-f", "{{.Dir}}", "golang.org/x/telemetry/internal/configgen").Output()
if err != nil {
return "", err
}
dir := strings.TrimSpace(string(out))
configFile := filepath.Join(dir, "..", "..", "config", "config.json")
if _, err := os.Stat(configFile); err != nil {
return "", err
}
return configFile, nil
}
func readConfig(file string) (*telemetry.UploadConfig, error) {
data, err := os.ReadFile(file)
if err != nil {
return nil, fmt.Errorf("reading config file: %v", err)
}
cfg := new(telemetry.UploadConfig)
if err := json.Unmarshal(data, cfg); err != nil {
return nil, fmt.Errorf("unmarshalling config file: %v", err)
}
return cfg, nil
}
// generate computes the upload config from chart configs and module
// information, returning the resulting formatted JSON.
func generate(gcfgs []chartconfig.ChartConfig, padding padding) (*telemetry.UploadConfig, error) {
ucfg := &telemetry.UploadConfig{
GOOS: goos(),
GOARCH: goarch(),
// the probability of uploading a report
SampleRate: SamplingRate,
}
var err error
ucfg.GoVersion, err = goVersions()
if err != nil {
return nil, fmt.Errorf("querying go info: %v", err)
}
for i, r := range gcfgs {
if err := ValidateChartConfig(r); err != nil {
// TODO(rfindley): this is a poor way to identify the faulty record. We
// should probably store position information in the ChartConfig.
return nil, fmt.Errorf("chart config #%d (%q): %v", i, r.Title, err)
}
}
var (
programs = make(map[string]*telemetry.ProgramConfig) // package path -> config
minVersions = make(map[string]string) // package path -> min version required, or "" for all
)
for _, gcfg := range gcfgs {
pcfg := programs[gcfg.Program]
if pcfg == nil {
pcfg = &telemetry.ProgramConfig{
Name: gcfg.Program,
}
programs[gcfg.Program] = pcfg
minVersions[gcfg.Program] = gcfg.Version
}
minVersions[gcfg.Program] = minVersion(minVersions[gcfg.Program], gcfg.Version)
ccfg := telemetry.CounterConfig{
Name: gcfg.Counter,
Rate: 1.0, // TODO(rfindley): how should rate be configured?
Depth: gcfg.Depth,
}
if gcfg.Depth > 0 {
pcfg.Stacks = append(pcfg.Stacks, ccfg)
} else {
pcfg.Counters = append(pcfg.Counters, ccfg)
}
}
for _, p := range programs {
minVersion := minVersions[p.Name]
// Collect eligible program versions. If p is a toolchain tool (cmd/go,
// cmd/compile, etc), these come out of the Go versions queried above.
// Otherwise, they come from the proxy.
//
// In both of these cases, the versions should be valid, but we verify
// anyway as otherwise the version comparison is meaningless.
// (and in fact, there is an invalid go1.9.2rc2 version in the proxy)
if telemetry.IsToolchainProgram(p.Name) {
// Note: no need to pad versions for toolchain programs, since the
// toolchain is released infrequently.
// (and in any case, version padding only works for semantic versions)
for _, v := range ucfg.GoVersion {
if !version.IsValid(v) {
// The proxy toolchain versions list go1.9.2rc2, which is invalid.
// Skip it.
continue
}
if minVersion == "" || version.Compare(minVersion, v) <= 0 {
p.Versions = append(p.Versions, v)
}
}
} else {
versions, err := listProxyVersions(p.Name)
if err != nil {
return nil, fmt.Errorf("listing versions for %q: %v", p.Name, err)
}
// Filter proxy versions in place.
i := 0
for _, v := range versions {
if !semver.IsValid(v) {
return nil, fmt.Errorf("invalid semver %q returned from proxy for %q", v, p.Name)
}
if minVersion == "" || semver.Compare(minVersion, v) <= 0 {
versions[i] = v
i++
}
}
p.Versions = padVersions(versions[:i], prereleasesForProgram(p.Name), padding)
}
ucfg.Programs = append(ucfg.Programs, p)
}
sort.Slice(ucfg.Programs, func(i, j int) bool {
return ucfg.Programs[i].Name < ucfg.Programs[j].Name
})
return ucfg, nil
}
// contains reports whether outer contains all program versions of inner, and
// is otherwise equivalent to inner.
func contains(outer, inner *telemetry.UploadConfig) bool {
if !slices.Equal(outer.GOARCH, inner.GOARCH) {
return false
}
if !slices.Equal(outer.GOOS, inner.GOOS) {
return false
}
if !slices.Equal(outer.GoVersion, inner.GoVersion) {
return false
}
for _, pi := range inner.Programs {
i := slices.IndexFunc(outer.Programs, func(po *telemetry.ProgramConfig) bool {
return po.Name == pi.Name
})
if i < 0 {
return false
}
po := outer.Programs[i]
if !sliceContains(po.Versions, pi.Versions) {
return false
}
if !slices.Equal(po.Counters, pi.Counters) {
return false
}
if !slices.Equal(po.Stacks, pi.Stacks) {
return false
}
}
for _, po := range outer.Programs {
if !slices.ContainsFunc(inner.Programs, func(pi *telemetry.ProgramConfig) bool {
return pi.Name == po.Name
}) {
return false
}
}
return true
}
func sliceContains[T comparable](outer, inner []T) bool {
m := toMap(outer)
for _, v := range inner {
if !m[v] {
return false
}
}
return true
}
func toMap[T comparable](s []T) map[T]bool {
m := make(map[T]bool)
for _, v := range s {
m[v] = true
}
return m
}
// prereleasesForProgram returns the set of prereleases to use for the provided
// program. We may need to customize this for the conventions of different
// programs.
func prereleasesForProgram(program string) []string {
// Surely eight prereleases is enough for any program... :)
return []string{"pre.1", "pre.2", "pre.3", "pre.4", "pre.5", "pre.6", "pre.7", "pre.8"}
}
// minVersion returns the lesser semantic version of v1 and v2.
//
// As a special case, the empty string is treated as an absolute minimum
// (empty => all versions are greater).
func minVersion(v1, v2 string) string {
if v1 == "" || v2 == "" {
return ""
}
if semver.Compare(v1, v2) > 0 {
return v2
}
return v1
}
// goos returns a sorted slice of known GOOS values.
func goos() []string {
var gooses []string
for goos := range knownOS {
gooses = append(gooses, goos)
}
sort.Strings(gooses)
return gooses
}
// goarch returns a sorted slice of known GOARCH values.
func goarch() []string {
var arches []string
for arch := range knownArch {
arches = append(arches, arch)
}
sort.Strings(arches)
return arches
}
// goInfo queries the proxy for information about go distributions, including
// versions, GOOS, and GOARCH values.
func goVersions() ([]string, error) {
// Trick: read Go distribution information from the module versions of
// golang.org/toolchain. These define the set of valid toolchains, and
// therefore are a reasonable source for version information.
//
// A more authoritative source for this information may be
// https://go.dev/dl?mode=json&include=all.
proxyVersions, err := listProxyVersions("golang.org/toolchain")
if err != nil {
return nil, fmt.Errorf("listing toolchain versions: %v", err)
}
var goVersionRx = regexp.MustCompile(`^-(go.+)\.[^.]+-[^.]+$`)
verSet := make(map[string]struct{})
for _, v := range proxyVersions {
pre := semver.Prerelease(v)
match := goVersionRx.FindStringSubmatch(pre)
if match == nil {
return nil, fmt.Errorf("proxy version %q does not match prerelease regexp %q", v, goVersionRx)
}
verSet[match[1]] = struct{}{}
}
var vers []string
for v := range verSet {
vers = append(vers, v)
}
sort.Sort(byGoVersion(vers))
return vers, nil
}
type byGoVersion []string
func (vs byGoVersion) Len() int { return len(vs) }
func (vs byGoVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
func (vs byGoVersion) Less(i, j int) bool {
cmp := version.Compare(vs[i], vs[j])
if cmp != 0 {
return cmp < 0
}
// To ensure that we have a stable sort, order equivalent Go versions lexically.
return vs[i] < vs[j]
}
// versionsForTesting contains versions to use for testing, rather than
// querying the proxy.
var versionsForTesting map[string][]string
// listProxyVersions queries the Go module mirror for published versions of the
// given modulePath.
//
// modulePath must be lower-case (or already escaped): this function doesn't do
// any escaping of upper-cased letters, as is required by the proxy prototol
// (https://go.dev/ref/mod#goproxy-protocol).
func listProxyVersions(modulePath string) ([]string, error) {
if vers, ok := versionsForTesting[modulePath]; ok {
return vers, nil
}
cmd := exec.Command("go", "list", "-m", "--versions", modulePath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("listing versions: %v (stderr: %v)", err, stderr.String())
}
fields := strings.Fields(strings.TrimSpace(string(out)))
if len(fields) == 0 {
return nil, fmt.Errorf("invalid version list output: %q", string(out))
}
return fields[1:], nil
}
// padding defines constraints on additional versions to pad.
//
// These constraints help restrict version padding to "reasonable" versions,
// based on heuristics such as "we never do more than 3 prereleases for a given
// version" or "we never have more than 5 patch versions" or "we can't do more
// than 10 total releases over that time period". See the field documentation
// for details.
type padding struct {
releases int // bounds on the total number of releases
maj int // bounds the number of new major versions
majmin int // bounds the number of new major+minor versions
patch int // bounds the number of new patch versions
pre int // the number of prereleases to pad per release
}
// padVersions pads the existing version list with potential next versions, so
// that we don't have to wait an additional day to start getting reports for a
// newly tagged version.
//
// The prereleases argument may be supplied to provide a set of potential
// prerelease candidates. For example, if the program releases prereleases of
// the form "-pre.N", prereleases should be {"pre.1", "pre.2", ...}. For each
// potential next release version, the next two prerelease versions will be
// selected out of the provided set of prereleases.
func padVersions(versions []string, prereleasePatterns []string, padding padding) []string {
versions = slices.Clone(versions)
semver.Sort(versions)
latestRelease := "v0.0.0"
all := make(map[string]bool) // for de-duplicating padded versions
for _, v := range versions {
cv := semver.Canonical(v)
all[cv] = true
if semver.Prerelease(cv) == "" && semver.Compare(latestRelease, cv) < 0 {
latestRelease = cv
}
}
parsedLatest, ok := parseSemver(latestRelease)
if !ok {
// "can't happen", since the latest release version should always be canonical.
panic(fmt.Sprintf("unable to parse latest release version %q", latestRelease))
}
// Pad the latest version only.
//
// This assumes that the program in question doesn't patch older releases
// (as is the case with gopls). If that assumption ever changes, we may need
// to apply padding to older versions as well.
versionsToPad := []semversion{parsedLatest}
var maj, min, patch int
for _, toPad := range versionsToPad {
for majPadding := 0; majPadding <= padding.maj; majPadding++ {
maj = toPad.major + majPadding
for minPadding := 0; minPadding+majPadding <= padding.majmin; minPadding++ {
if majPadding == 0 {
min = toPad.minor + minPadding
} else {
min = minPadding
}
for patchPadding := 0; patchPadding <= padding.patch; patchPadding++ {
releases := majPadding + minPadding + patchPadding
if releases == 0 || releases > padding.releases {
continue
}
if majPadding == 0 && minPadding == 0 {
patch = toPad.patch + patchPadding
} else {
patch = patchPadding
}
v := fmt.Sprintf("v%d.%d.%d", maj, min, patch)
if all[v] {
// This guard is future proofing: we may have seen this version
// before if we are ever padding something other than the latest
// version.
continue
}
versions = append(versions, v)
// We may already have prereleases at this version. Don't pad
// additional prereleases, under the assumption that we don't
// typically have more than padding.pre prereleases.
nextPrerelease := 0
for i, patt := range prereleasePatterns {
pre := fmt.Sprintf("%s-%s", v, patt)
if all[pre] {
nextPrerelease = i + 1
}
}
for i := nextPrerelease; i < len(prereleasePatterns) && i < padding.pre; i++ {
pre := fmt.Sprintf("%s-%s", v, prereleasePatterns[i])
versions = append(versions, pre)
}
}
}
}
}
semver.Sort(versions)
return versions
}
// version is a parsed semantic version.
type semversion struct {
major, minor, patch int
pre string
}
// parseSemver attempts to parse semver components out of the provided semver
// v. If v is not valid semver in canonical form, parseSemver returns _, _, _,
// _, false.
func parseSemver(v string) (_ semversion, ok bool) {
var parsed semversion
v, parsed.pre, _ = strings.Cut(v, "-")
if _, err := fmt.Sscanf(v, "v%d.%d.%d", &parsed.major, &parsed.minor, &parsed.patch); err == nil {
ok = true
}
return parsed, ok
}
@@ -0,0 +1,256 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.22
package main
import (
_ "embed"
"reflect"
"sort"
"testing"
"golang.org/x/telemetry/internal/chartconfig"
"golang.org/x/telemetry/internal/telemetry"
)
func TestGenerate(t *testing.T) {
defer func(vers map[string][]string) {
versionsForTesting = vers
}(versionsForTesting)
versionsForTesting = map[string][]string{
"golang.org/toolchain": {"v0.0.1-go1.21.0.linux-arm", "v0.0.1-go1.20.linux-arm"},
"golang.org/x/tools/gopls": {"v0.13.0", "v0.14.0", "v0.15.0-pre.1", "v0.15.0"},
}
const raw = `
title: Editor Distribution
counter: gopls/editor:{emacs,vim,vscode,other}
description: measure editor distribution for gopls users.
type: partition
issue: https://go.dev/issue/61038
program: golang.org/x/tools/gopls
version: v0.14.0
`
gcfgs, err := chartconfig.Parse([]byte(raw))
if err != nil {
t.Fatal(err)
}
got, err := generate(gcfgs, padding{2, 1, 1, 2, 2})
if err != nil {
t.Fatal(err)
}
want := telemetry.UploadConfig{
GOOS: goos(),
GOARCH: goarch(),
SampleRate: SamplingRate,
GoVersion: []string{"go1.20", "go1.21.0"},
Programs: []*telemetry.ProgramConfig{{
Name: "golang.org/x/tools/gopls",
Versions: []string{
"v0.14.0",
"v0.15.0-pre.1",
"v0.15.0",
"v0.15.1-pre.1",
"v0.15.1-pre.2",
"v0.15.1",
"v0.15.2-pre.1",
"v0.15.2-pre.2",
"v0.15.2",
"v0.16.0-pre.1",
"v0.16.0-pre.2",
"v0.16.0",
"v0.16.1-pre.1",
"v0.16.1-pre.2",
"v0.16.1",
"v1.0.0-pre.1",
"v1.0.0-pre.2",
"v1.0.0",
"v1.0.1-pre.1",
"v1.0.1-pre.2",
"v1.0.1",
},
Counters: []telemetry.CounterConfig{{
Name: "gopls/editor:{emacs,vim,vscode,other}",
Rate: 1.0,
}},
}},
}
if !reflect.DeepEqual(*got, want) {
if len(got.Programs) != len(want.Programs) {
t.Errorf("generate(): got %d programs, want %d", len(got.Programs), len(want.Programs))
} else {
for i, gotp := range got.Programs {
want := *want.Programs[i]
if !reflect.DeepEqual(*gotp, want) {
t.Errorf("generate() program #%d =\n%+v\nwant:\n%+v", i, *gotp, want)
}
}
}
t.Errorf("generate() =\n%+v\nwant:\n%+v", *got, want)
}
}
func TestByGoVersion_Less(t *testing.T) {
got := []string{
"go1.21.0",
"go1.21rc1",
"go1.9",
"go1.9rc1",
"go1.6",
"go1.6beta1",
}
want := []string{
"go1.6beta1",
"go1.6",
"go1.9rc1",
"go1.9",
"go1.21rc1",
"go1.21.0",
}
sort.Sort(byGoVersion(got))
if !reflect.DeepEqual(got, want) {
t.Errorf("sort.Sort(byGoVersion(got)) = %v, want %v", got, want)
}
}
func TestContains(t *testing.T) {
baseline := func() *telemetry.UploadConfig {
return &telemetry.UploadConfig{
GOOS: goos(),
GOARCH: goarch(),
GoVersion: []string{"go1.20", "go1.21.0"},
Programs: []*telemetry.ProgramConfig{{
Name: "golang.org/x/tools/gopls",
Versions: []string{
"v0.14.0",
"v0.15.0-pre.1",
"v0.15.0",
"v0.15.1-pre.1",
"v0.15.1-pre.2",
"v0.15.1",
},
Counters: []telemetry.CounterConfig{{
Name: "gopls/editor:{emacs,vim,vscode,other}",
Rate: 1.0,
}},
}},
}
}
tests := []struct {
name string
outerMut, innerMut func(*telemetry.UploadConfig)
want bool
}{
{
"additional arch",
func(cfg *telemetry.UploadConfig) { cfg.GOARCH = append(cfg.GOARCH, "fake") },
func(cfg *telemetry.UploadConfig) {},
false,
},
{
"additional program",
func(cfg *telemetry.UploadConfig) { cfg.Programs = append(cfg.Programs, new(telemetry.ProgramConfig)) },
func(cfg *telemetry.UploadConfig) {},
false,
},
{
"additional counter",
func(cfg *telemetry.UploadConfig) {
cfg.Programs[0].Counters = append(cfg.Programs[0].Counters, telemetry.CounterConfig{})
},
func(cfg *telemetry.UploadConfig) {},
false,
},
{
"additional version",
func(cfg *telemetry.UploadConfig) {
cfg.Programs[0].Versions = append(cfg.Programs[0].Versions, "v99.99.99")
},
func(cfg *telemetry.UploadConfig) {},
true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
outer := baseline()
test.outerMut(outer)
inner := baseline()
test.innerMut(inner)
if got := contains(outer, inner); got != test.want {
t.Errorf("contains(...) = %v, want %v", got, test.want)
}
})
}
}
func TestPadVersions(t *testing.T) {
tests := []struct {
versions []string
prereleasePatterns []string
padding padding
want []string
}{
{
nil,
[]string{"pre.1"},
padding{1, 1, 1, 1, 2},
[]string{
"v0.0.1-pre.1",
"v0.0.1",
"v0.1.0-pre.1",
"v0.1.0",
"v1.0.0-pre.1",
"v1.0.0",
},
},
{
[]string{"v0.8.3", "v0.9.1", "v0.9.2", "v1.0.0", "v1.0.1", "v1.0.2-pre.1", "v1.0.2-pre.2", "v1.0.2-pre.3"},
[]string{"pre.1", "pre.2", "pre.3", "pre.4"},
padding{2, 1, 2, 2, 2},
[]string{
"v0.8.3",
"v0.9.1",
"v0.9.2",
"v1.0.0",
"v1.0.1",
"v1.0.2-pre.1",
"v1.0.2-pre.2",
"v1.0.2-pre.3",
"v1.0.2",
"v1.0.3-pre.1",
"v1.0.3-pre.2",
"v1.0.3",
"v1.1.0-pre.1",
"v1.1.0-pre.2",
"v1.1.0",
"v1.1.1-pre.1",
"v1.1.1-pre.2",
"v1.1.1",
"v1.2.0-pre.1",
"v1.2.0-pre.2",
"v1.2.0",
"v2.0.0-pre.1",
"v2.0.0-pre.2",
"v2.0.0",
"v2.0.1-pre.1",
"v2.0.1-pre.2",
"v2.0.1",
"v2.1.0-pre.1",
"v2.1.0-pre.2",
"v2.1.0",
},
},
}
for _, test := range tests {
got := padVersions(test.versions, test.prereleasePatterns, test.padding)
if !reflect.DeepEqual(got, test.want) {
t.Errorf("padVersions(%v, %v) =\n%v\nwant:\n%v", test.versions, test.prereleasePatterns, got, test.want)
}
}
}
@@ -0,0 +1,60 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.22
package main
// knownOS is the list of past, present, and future known GOOS values.
// Do not remove from this list, as it is used for filename matching.
// If you add an entry to this list, look at unixOS, below.
var knownOS = map[string]bool{
"aix": true,
"android": true,
"darwin": true,
"dragonfly": true,
"freebsd": true,
"hurd": true,
"illumos": true,
"ios": true,
"js": true,
"linux": true,
"nacl": true,
"netbsd": true,
"openbsd": true,
"plan9": true,
"solaris": true,
"wasip1": true,
"windows": true,
"zos": true,
}
// knownArch is the list of past, present, and future known GOARCH values.
// Do not remove from this list, as it is used for filename matching.
var knownArch = map[string]bool{
"386": true,
"amd64": true,
"amd64p32": true,
"arm": true,
"armbe": true,
"arm64": true,
"arm64be": true,
"loong64": true,
"mips": true,
"mipsle": true,
"mips64": true,
"mips64le": true,
"mips64p32": true,
"mips64p32le": true,
"ppc": true,
"ppc64": true,
"ppc64le": true,
"riscv": true,
"riscv64": true,
"s390": true,
"s390x": true,
"sparc": true,
"sparc64": true,
"wasm": true,
}
@@ -0,0 +1,55 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.22
package main
import (
"errors"
"fmt"
"go/version"
"golang.org/x/mod/semver"
"golang.org/x/telemetry/internal/chartconfig"
"golang.org/x/telemetry/internal/telemetry"
)
// ValidateChartConfig checks that a ChartConfig is complete and coherent,
// returning an error describing all problems encountered, or nil.
func ValidateChartConfig(cfg chartconfig.ChartConfig) error {
var errs []error
reportf := func(format string, args ...any) {
errs = append(errs, fmt.Errorf(format, args...))
}
if cfg.Title == "" {
reportf("title must be set")
}
if len(cfg.Issue) == 0 {
reportf("at least one issue is required")
}
if cfg.Program == "" {
reportf("program must be set")
}
if cfg.Counter == "" {
reportf("counter must be set")
}
if cfg.Type == "" {
reportf("type must be set")
}
if cfg.Depth < 0 {
reportf("invalid depth %d: must be non-negative", cfg.Depth)
}
if cfg.Depth != 0 && cfg.Type != "stack" {
reportf("depth can only be set for \"stack\" chart types")
}
valid := semver.IsValid
if telemetry.IsToolchainProgram(cfg.Program) {
valid = version.IsValid
}
if cfg.Version != "" && !valid(cfg.Version) {
reportf("%q is not a valid version (must be a go version or semver)", cfg.Version)
}
return errors.Join(errs...)
}
@@ -0,0 +1,81 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.22
package main
import (
"strings"
"testing"
"golang.org/x/telemetry/internal/chartconfig"
)
func TestLoadedChartsAreValid(t *testing.T) {
// Test that we can actually load the chart config.
charts, err := chartconfig.Load()
if err != nil {
t.Errorf("Load() failed: %v", err)
}
for i, chart := range charts {
if err := ValidateChartConfig(chart); err != nil {
t.Errorf("Chart %d is invalid: %v", i, err)
}
}
}
func TestValidateOK(t *testing.T) {
// A minimally valid chart config.
const input = `
title: Editor Distribution
counter: gopls/editor:{emacs,vim,vscode,other}
type: partition
issue: https://go.dev/issue/12345
program: golang.org/x/tools/gopls
`
records, err := chartconfig.Parse([]byte(input))
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("Parse(%q) returned %d records, want exactly 1", input, len(records))
}
if err := ValidateChartConfig(records[0]); err != nil {
t.Errorf("Validate(%q) = %v, want nil", input, err)
}
}
func TestValidate(t *testing.T) {
tests := map[string][]string{ // input -> want errors
// validation of mandatory fields
"description:bar": {"title", "program", "issue", "counter", "type"},
// validation of semver intervals
"version:1.2.3.4": {"semver"},
// valid of stack configuration
"depth:-1": {"non-negative", "stack"},
}
for input, wantErrs := range tests {
records, err := chartconfig.Parse([]byte(input))
if err != nil {
t.Fatal(err)
}
if len(records) != 1 {
t.Fatalf("Parse(%q) returned %d records, want exactly 1", input, len(records))
}
err = ValidateChartConfig(records[0])
if err == nil {
t.Fatalf("Validate(%q) succeeded unexpectedly", input)
}
errs := err.Error()
for _, want := range wantErrs {
if !strings.Contains(errs, want) {
t.Errorf("Validate(%q) = %v, want containing %q", input, err, want)
}
}
}
}
@@ -0,0 +1,86 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package configstore abstracts interaction with the telemetry config server.
// Telemetry config (golang.org/x/telemetry/config) is distributed as a go
// module containing go.mod and config.json. Programs that upload collected
// counters download the latest config using `go mod download`. This provides
// verification of downloaded configuration and cacheability.
package configstore
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"sync/atomic"
"golang.org/x/telemetry/internal/telemetry"
)
const (
ModulePath = "golang.org/x/telemetry/config"
configFileName = "config.json"
)
// needNoConsole is used on windows to set the windows.CREATE_NO_WINDOW
// creation flag.
var needNoConsole = func(cmd *exec.Cmd) {}
var downloads int64
// Downloads reports, for testing purposes, the number of times [Download] has
// been called.
func Downloads() int64 {
return atomic.LoadInt64(&downloads)
}
// Download fetches the requested telemetry UploadConfig using "go mod
// download". If envOverlay is provided, it is appended to the environment used
// for invoking the go command.
//
// The second result is the canonical version of the requested configuration.
func Download(version string, envOverlay []string) (*telemetry.UploadConfig, string, error) {
atomic.AddInt64(&downloads, 1)
if version == "" {
version = "latest"
}
modVer := ModulePath + "@" + version
var stdout, stderr bytes.Buffer
cmd := exec.Command("go", "mod", "download", "-json", modVer)
needNoConsole(cmd)
cmd.Env = append(os.Environ(), envOverlay...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
var info struct {
Error string
}
if err := json.Unmarshal(stdout.Bytes(), &info); err == nil && info.Error != "" {
return nil, "", fmt.Errorf("failed to download config module: %v", info.Error)
}
return nil, "", fmt.Errorf("failed to download config module: %w\n%s", err, &stderr)
}
var info struct {
Dir string
Version string
Error string
}
if err := json.Unmarshal(stdout.Bytes(), &info); err != nil || info.Dir == "" {
return nil, "", fmt.Errorf("failed to download config module (invalid JSON): %w", err)
}
data, err := os.ReadFile(filepath.Join(info.Dir, configFileName))
if err != nil {
return nil, "", fmt.Errorf("invalid config module: %w", err)
}
cfg := new(telemetry.UploadConfig)
if err := json.Unmarshal(data, cfg); err != nil {
return nil, "", fmt.Errorf("invalid config: %w", err)
}
return cfg, info.Version, nil
}
@@ -0,0 +1,77 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package configstore_test
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
"golang.org/x/telemetry/internal/configstore"
"golang.org/x/telemetry/internal/configtest"
"golang.org/x/telemetry/internal/telemetry"
"golang.org/x/telemetry/internal/testenv"
)
func TestDownload(t *testing.T) {
testenv.NeedsGo(t)
configVersion := "v0.1.0"
in := &telemetry.UploadConfig{
GOOS: []string{"darwin"},
GOARCH: []string{"amd64", "arm64"},
GoVersion: []string{"1.20.3", "1.20.4"},
Programs: []*telemetry.ProgramConfig{{
Name: "gopls",
Versions: []string{"v0.11.0"},
Counters: []telemetry.CounterConfig{{
Name: "foobar",
Rate: 2,
}},
}},
}
env := configtest.LocalProxyEnv(t, in, configVersion)
testCases := []struct {
version string
want telemetry.UploadConfig
}{
{version: configVersion, want: *in},
{version: "latest", want: *in},
}
for _, tc := range testCases {
t.Run(tc.version, func(t *testing.T) {
got, _, err := configstore.Download(tc.version, env)
if err != nil {
t.Fatal("failed to download:", err)
}
want := tc.want
if !reflect.DeepEqual(*got, want) {
t.Errorf("Download(latest, _) = %v\nwant %v", stringify(got), stringify(want))
}
})
}
t.Run("invalidversion", func(t *testing.T) {
got, ver, err := configstore.Download("nonexisting", env)
if err == nil {
t.Fatalf("download succeeded unexpectedly: %v %+v", ver, got)
}
if !strings.Contains(err.Error(), "invalid version") {
t.Errorf("unexpected error message: %v", err)
}
})
}
func stringify(x any) string {
ret, err := json.MarshalIndent(x, "", " ")
if err != nil {
return fmt.Sprintf("json.Marshal failed - %v", err)
}
return string(ret)
}
@@ -0,0 +1,33 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package configstore
import (
"os/exec"
"syscall"
"golang.org/x/sys/windows"
)
func init() {
needNoConsole = needNoConsoleWindows
}
func needNoConsoleWindows(cmd *exec.Cmd) {
// The uploader main process is likely a daemonized process with no console.
// (see x/telemetry/start_windows.go) The console creation behavior when
// a parent is a console process without console is not clearly documented
// but empirically we observed the new console is created and attached to the
// subprocess in the default setup.
//
// Ensure no new console is attached to the subprocess by setting CREATE_NO_WINDOW.
// https://learn.microsoft.com/en-us/windows/console/creation-of-a-console
// https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
cmd.SysProcAttr = &syscall.SysProcAttr{
CreationFlags: windows.CREATE_NO_WINDOW,
}
}
@@ -0,0 +1,59 @@
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package configtest provides a helper for testing using a local proxy
// containing a fake upload config.
package configtest
import (
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"testing"
"golang.org/x/telemetry/internal/configstore"
"golang.org/x/telemetry/internal/proxy"
"golang.org/x/telemetry/internal/telemetry"
)
// LocalProxyEnv writes a proxy directory for the given upload config, and
// returns a go environment to use for fetching that config from a local
// file-based proxy.
//
// This environment should be passed to [configstore.Download].
func LocalProxyEnv(t *testing.T, cfg *telemetry.UploadConfig, version string) []string {
t.Helper()
dir := t.TempDir()
encoded, err := json.Marshal(cfg)
if err != nil {
t.Fatalf("marshaling config failed: %v", err)
}
dirPath := fmt.Sprintf("%v@%v/", configstore.ModulePath, version)
files := map[string][]byte{
dirPath + "go.mod": []byte("module " + configstore.ModulePath + "\n\ngo 1.20\n"),
dirPath + "config.json": encoded,
}
proxyURI, err := proxy.WriteProxy(filepath.Join(dir, "proxy"), files)
if err != nil {
t.Fatalf("writing proxy failed: %v", err)
}
env := []string{
"GOPROXY=" + proxyURI, // Use the fake proxy.
"GONOSUMDB=*", // Skip verifying checksum against sum.golang.org.
"GOMODCACHE=" + filepath.Join(dir, "modcache"), // Don't pollute system module cache.
}
t.Cleanup(func() {
cmd := exec.Command("go", "clean", "-modcache")
cmd.Env = append(cmd.Environ(), env...)
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("go clean -modcache failed: %v\n%s", err, out)
}
})
return env
}
@@ -0,0 +1,20 @@
# Telemetry Content
This directory contains the templates, styles, scripts, and images used in the
telemetry services. Scripts and styles are transformed and minified by the
generator in [content.go](./content.go).
## Scripts & Styles
The generator command will look for entrypoint scripts and styles, i.e. files
that are not prefixed with an underscore, and minfiy their contents. TypeScript
files are also transformed into JavaScript. See
[devtools/cmd/esbuild](../devtools/cmd/esbuild/main.go) for more information.
## Templates
Use the .html extension to create a new route, or put an index.html file in a
directory with the desired path. Partial templates with the extension .tmpl in
the same directory as the requested page are included in the html/template
execution step to allow for sharing and composing multiple templates. See
[internal/content](../internal/content/content.go) for more information.
@@ -0,0 +1,54 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package content
import (
"embed"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
)
//go:embed *
var FS embed.FS
//go:generate go run generate.go
// RunESBuild runs esbuild for all content directories.
// If watch is set, RunESBuild instructs esbuild to watch the content
// directories, and runs esbuild in a separate goroutine.
func RunESBuild(watch bool) {
_, file, _, _ := runtime.Caller(0)
curDir := filepath.Dir(file)
cmdDir := filepath.Join(curDir, "..", "..", "godev", "devtools", "cmd", "esbuild")
for _, dir := range []string{"gotelemetryview", "shared", "telemetrygodev"} {
d := filepath.Join(curDir, dir)
args := []string{"run", ".", "--outdir", filepath.Join(d, "static")}
if watch {
args = append(args, "--watch", d)
}
args = append(args, d)
cmd := exec.Command("go", args...)
cmd.Dir = cmdDir
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Start(); err != nil {
log.Fatal(err)
}
if watch {
go func() {
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
}()
} else {
if err := cmd.Wait(); err != nil {
log.Fatal(err)
}
}
}
}
@@ -0,0 +1,13 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build ignore
package main
import "golang.org/x/telemetry/internal/content"
func main() {
content.RunESBuild(false)
}
@@ -0,0 +1,16 @@
{{define "info-icon"}}
<details class="go-Tooltip js-tooltip" data-gtmc="tooltip">
<summary>
<img
class="go-Icon"
height="20"
width="20"
src="/static/info_black_24dp.svg"
alt=""
/>
</summary>
<p>
{{.}}
</p>
</details>
{{end}}
@@ -0,0 +1,173 @@
/*!
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
@import url("../shared/base.css");
html {
scroll-padding-top: 4rem;
}
/* TODO(rfindley): refactor to share breadcrumb logic with telemetry.go.dev */
.ViewBreadcrumb {
position: sticky;
top: 0;
z-index: 1000;
}
.ViewBreadcrumb ol {
align-items: center;
border-bottom: var(--border);
display: inline-flex;
gap: 1rem;
list-style: none;
margin-block-start: 0;
margin-block-end: 0;
padding-inline-start: 0;
min-height: 3rem;
width: calc(100% - 2rem);
background-color: var(--color-background);
padding: 0 1rem;
font-size: 0.875rem;
position: fixed;
top: 0;
transition: top 0.1s ease-in 0.1s;
}
.ViewBreadcrumb ol:empty {
top: -3.0625rem;
}
.ViewBreadcrumb li:not(:last-child)::after {
content: ">";
margin-left: 1rem;
}
.ViewBreadcrumb li:last-child a {
color: var(--color-text-subtle);
}
.Index {
line-height: 1.5;
}
.Counters {
border: var(--border);
border-radius: 0.25rem;
display: grid;
gap: 1rem 2rem;
margin-top: 1rem;
overflow: auto;
padding: 1rem;
grid-template-areas:
"meta count count"
"stack stack stack"
"summary summary summary";
grid-auto-columns: 1fr 2fr 1fr;
}
.Meta {
grid-area: meta;
display: grid;
grid-auto-rows: min-content;
grid-template-columns: repeat(2, max-content);
gap: 0.5rem;
}
.Stack {
grid-area: stack;
border-top: var(--border);
padding-top: 1rem;
gap: 0.5rem 1rem;
display: flex;
flex-direction: column;
width: 100%;
}
.Stack summary {
display: block;
}
.Stack details .Count-entry:first-child::before {
content: "⏵";
}
.Stack details[open] .Count-entry:first-child::before {
content: "⏷";
}
.Count {
grid-area: count;
display: grid;
flex-grow: 1;
grid-auto-rows: min-content;
grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
gap: 0.5rem 1rem;
}
.Summary {
border-top: var(--border);
font-size: 0.875rem;
grid-area: summary;
line-height: 1.5;
padding-top: 1rem;
}
.Meta .unknown,
.Count .unknown,
.Stack .unknown {
color: var(--color-text-subtle);
}
.Count-entry {
display: flex;
gap: 0.25rem;
justify-content: space-between;
}
.Count-entry > span:nth-child(odd) {
overflow: hidden;
white-space: nowrap;
}
.Count-entry:not(.unknown) > span:nth-child(even) {
text-align: right;
color: var(--color-code-comment);
}
.Count-entry > span:nth-child(odd)::after {
content: " ----------------------------------------------------------------------------------------------- ";
letter-spacing: 0.125rem;
}
h2::after {
content: "⏷";
padding-left: 0.5rem;
}
html[data-closed-sections*="index"] h2#index::after,
html[data-closed-sections*="config"] h2#config::after,
html[data-closed-sections*="files"] h2#files::after,
html[data-closed-sections*="charts"] h2#charts::after,
html[data-closed-sections*="reports"] h2#reports::after {
content: "⏵";
}
html[data-closed-sections*="index"] h2#index ~ *,
html[data-closed-sections*="config"] h2#config ~ *,
html[data-closed-sections*="files"] h2#files ~ *,
html[data-closed-sections*="charts"] h2#charts ~ *,
html[data-closed-sections*="reports"] h2#reports ~ * {
display: none;
}
div[data-chart-id] {
min-height: 16rem;
}
/* Fix tooltip background for dark theme */
svg g[aria-label="tip"] g {
fill: var(--color-background);
}
@@ -0,0 +1,270 @@
<!--
Copyright 2023 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go Telemetry</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" href="/static/index.min.css">
<script src="/static/storage.min.js"></script>
</head>
<body>
<main id="main">
<!-- TODO(rfindley): refactor to share breadcrumbs with telemetry.go.dev -->
<nav class="ViewBreadcrumb js-breadcrumb">
<ol></ol>
</nav>
<div class="Container">
<div class="Content">
<h1 class="Title">Go Telemetry</h1>
<p>
This page allows you to inspect counters collected by Go Toolchain
programs on your machine. It includes counters for submitted and
pending reports. For more information about Go Toolchain telemetry
<a target="_blank" rel="noreferrer" href="https://telemetry.go.dev/privacy">
read the docs here.
</a>
</p>
<section class="Index">
<h2 id="index">Index</h2>
<ul>
<li>
<a href="#charts">Charts</a>
<ul>
{{range .Charts.Programs}}
<li>
<a href="#{{.ID}}">{{.Name}}</a>
<ul>
{{range .Counters}}
<li>
<a href="#{{.ID}}">{{.Name}}</a>
</li>
{{end}}
</ul>
</li>
{{end}}
</ul>
</li>
<li>
<a href="#config">Config</a>
</li>
<li>
<a href="#files">Counters</a>
<ul>
{{range .Files}}
<li>
<a href="#{{.ID}}"> {{.ID}} </a>
</li>
{{end}}
</ul>
</li>
<li>
<a href="#reports">Reports</a>
<ul style="column-count: auto; column-width: 10rem">
{{range .Reports}}
<li>
<a href="#{{.ID}}">{{.Week}}</a>
</li>
{{end}}
</ul>
</li>
</ul>
</section>
<section class="Charts">
<h2 id="charts">Charts</h2>
<p>
Charts are visualizations of the counters from your archived
reports. Counters for different program builds of the same program
are summed together. Use the index to navigate to charts by
counter name.
</p>
{{range .Charts.Programs}}
<div class="Chart">
{{$pname := .Name}}
<h3 id="{{.ID}}" data-label="{{$pname}}">
{{$pname}}
{{if not .Active}}
{{template "info-icon" "This program is not present in the telemetry config."}}
{{end}}
</h3>
{{range .Counters}}
<div>
{{$cname := .Name}}
<h4 id="{{.ID}}" data-label="{{$cname}}">
{{$cname}}
{{if not .Active}}
{{template "info-icon" "This counter is not present in the telemetry config."}}
{{end}}
</h4>
<div data-chart-id="{{.ID}}"></div>
</div>
{{end}}
</div>
{{end}}
</section>
<section class="Config">
<h2 id="config">Config</h2>
<p>
The config contains the list of active counters for each program
and allowed report metadata.
</p>
<label>
Version
<select class="js-selectConfig" name="config">
{{range .ConfigVersions}}
<option value="{{.}}" {{if eq . $.RequestedConfig}}selected{{end}}>
{{.}}
</option>
{{end}}
</select>
</label>
<pre style="max-height: 20rem">{{.PrettyConfig}}</pre>
</section>
<section class="Files">
<h2 id="files">Counters</h2>
<p>
Counters display data from active counter files that has not yet
been uploaded with a report or archived. If a report includes data
that is not registered in the telemetry config, a summary of those
fields and how they'll be handled appears next to the counter
values.
</p>
{{range .Files}}
<div class="File">
<h3 id="{{.ID}}">{{.ID}}</h3>
<div class="Counters">
<div class="Meta">
<span>Program:</span>
<span class="{{if not .ActiveMeta.Program}}unknown{{end}}">
{{.Meta.Program}}
</span>
<span>Version:</span>
<span class="{{if not .ActiveMeta.Version}}unknown{{end}}">
{{.Meta.Version}}
</span>
<span>GOOS:</span>
<span class="{{if not .ActiveMeta.GOOS}}unknown{{end}}">
{{.Meta.GOOS}}
</span>
<span>GOARCH:</span>
<span class="{{if not .ActiveMeta.GOARCH}}unknown{{end}}">
{{.Meta.GOARCH}}
</span>
<span>GoVersion:</span>
<span class="{{if not .ActiveMeta.GoVersion}}unknown{{end}}">
{{.Meta.GoVersion}}
</span>
<span>TimeBegin:</span>
<span>{{.Meta.TimeBegin}}</span>
<span>TimeEnd:</span>
<span>{{.Meta.TimeEnd}}</span>
</div>
{{$file := .}}
{{with .Counts}}
<div class="Count">
{{range .}}
<div class="Count-entry {{if not .Active }}unknown{{end}}">
<span>{{.Name}}</span><span>{{.Value}}</span>
</div>
{{end}}
</div>
{{end}}
{{with .Stacks}}
<div class="Stack">
Call stacks:
{{range .}}
<details>
<summary>
<div class="Count-entry {{if not .Active }}unknown{{end}}">
<span>{{.Name}}</span><span>{{.Value}}</span>
</div>
</summary>
<pre>{{.Trace}}</pre>
</details>
{{end}}
</div>
{{end}}
{{with .Summary}}
<div class="Summary">{{.}}</div>
{{end}}
</div>
</div>
{{end}}
</section>
<section class="Reports">
<h2 id="reports">Reports</h2>
<p>
Reports represent local copies of the data uploaded by the Go
command to telemetry.go.dev. Use the index to navigate to a
report by upload date or program build.
</p>
{{range .Reports}}
<div class="Report">
{{$date := .Week}}
<h3 id="reports:{{$date}}">{{$date}}</h3>
{{range .Programs}}
<div id="{{.ID}}" class="Counters">
<div class="Meta">
<span>Program:</span>
<span class="{{if not ($.Config.HasProgram .Program)}}unknown{{end}}">
{{.Program}}
</span>
<span>Version:</span>
<span class="{{if not ($.Config.HasVersion .Program .Version)}}unknown{{end}}">
{{.Version}}
</span>
<span>GOOS:</span>
<span class="{{if not ($.Config.HasGOOS .GOOS)}}unknown{{end}}">
{{.GOOS}}
</span>
<span>GOARCH:</span>
<span class="{{if not ($.Config.HasGOARCH .GOARCH)}}unknown{{end}}">
{{.GOARCH}}
</span>
<span>GoVersion:</span>
<span class="{{if not ($.Config.HasGoVersion .GoVersion)}}unknown{{end}}">
{{.GoVersion}}
</span>
</div>
{{$report := .}}
{{with .Counters}}
<div class="Count">
{{range $name, $value := .}}
<div class="Count-entry {{if not ($.Config.HasCounter $report.Program $name) }}unknown{{end}}">
<span>{{$name}}</span><span>{{$value}}</span>
</div>
{{end}}
</div>
{{end}}
{{with .Summary}}
<div class="Summary">{{.}}</div>
{{end}}
</div>
{{end}}
</div>
{{end}}
</section>
</div>
</div>
</main>
<script>
window.Page = {{.}};
</script>
<script src="/static/index.min.js"></script>
</body>
</html>
@@ -0,0 +1,184 @@
/**
* @license
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
import * as Plot from "@observablehq/plot";
import { debounce } from "../shared/treenav";
declare global {
interface Page {
Charts: ChartData;
}
interface ChartData {
Programs: Program[];
DateRange: [string, string];
UploadDay: Plot.TimeIntervalName;
}
interface Program {
ID: string;
Name: string;
Counters: Counter[];
Active: boolean;
}
interface Counter {
ID: string;
Name: string;
Data: Datum[];
}
interface Datum {
[key: string]: any;
Week: string;
Program: string;
Version: string;
GOARCH: string;
GOOS: string;
GoVersion: string;
Key: string;
Value: number;
}
const Page: Page;
}
window.onload = function () {
drawCharts();
configSelector();
breadcrumbController();
sectionController();
};
// sectionController adds event listeners to the section headers
// to toggle them open and closed.
function sectionController() {
const html = document.querySelector("html")!;
for (const e of document.querySelectorAll("h2")) {
e.addEventListener("click", function () {
let closed = localStorage.getItem("closed-sections")?.split(",");
if (closed?.includes(this.id)) {
closed = closed.filter((v) => v !== this.id);
const str = closed.join(",");
localStorage.setItem("closed-sections", str);
html.setAttribute("data-closed-sections", str);
} else {
closed = [this.id].concat(closed ?? []);
const str = closed.join(",");
localStorage.setItem("closed-sections", str);
html.setAttribute("data-closed-sections", str);
}
});
}
}
// drawCharts draws the charts using @observable/plot. It is called when
// the page is first rendered and when a facet is selected.
function drawCharts() {
for (const program of Page.Charts.Programs ?? []) {
for (const counter of program.Counters ?? []) {
const rectYOpts: Plot.BinXInputs<Plot.RectYOptions> = {
tip: true,
x: (d: Datum) => new Date(d.Week),
y: (d: Datum) => d.Value,
interval: Page.Charts.UploadDay,
fill: (d: Datum) => {
const n = Number(d.Key);
return isNaN(n) ? d.Key : n;
},
};
const chart = Plot.plot({
nice: true,
x: {
type: "utc",
domain: Page.Charts.DateRange.map((d) => new Date(d)),
label: "Week",
},
y: {
label: "Value",
},
color: {
type: "ordinal",
legend: true,
scheme: "Spectral",
reverse: true,
label: "Counter",
},
height: 256,
style: "overflow:visible;width:100%;background:transparent",
marks: [
Plot.rectY(counter.Data, Plot.binX({ y: "sum" }, rectYOpts)),
Plot.ruleY([0]),
],
});
document
.querySelector(`[data-chart-id="${counter.ID}"]`)
?.replaceChildren(chart);
}
}
}
// configSelector adds an event listener that reloads the page when a config
// version is selected.
function configSelector() {
const el = document.querySelector<HTMLButtonElement>(".js-selectConfig");
el?.addEventListener("change", () => {
const params = new URLSearchParams(location.search);
params.set(el.name, el.value);
history.replaceState(null, "", "?" + params.toString());
location.reload();
});
}
// breadcrumbController updates the navigation header as the user scrolls
// that page displaying information about the content currently in the
// viewport.
function breadcrumbController() {
const headings =
document.querySelectorAll<HTMLHeadingElement>("h1, h2, h3, h4");
const callback = debounce(() => {
let above: HTMLHeadingElement[] = [];
for (const h of headings) {
const rect = h.getBoundingClientRect();
if (rect.height && rect.top < 80) {
above.unshift(h);
}
}
if (above.length < 2) {
above = [];
}
let threshold = Infinity;
const els: HTMLHeadingElement[] = [];
for (const h of above) {
const level = Number(h.tagName[1]);
if (level < threshold) {
threshold = level;
els.unshift(h);
}
}
const breadcrumb = document.querySelector(".js-breadcrumb ol");
const items = [];
for (const h of els) {
breadcrumb?.replaceChildren;
const li = document.createElement("li");
const a = document.createElement("a");
a.href = `#${h.id}`;
a.innerText = h.getAttribute("data-label") ?? h.innerText;
li.appendChild(a);
items.push(li);
}
breadcrumb?.replaceChildren(...items);
}, 100);
const observer = new IntersectionObserver(callback);
for (const h of headings) {
observer.observe(h);
}
}
export {};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>

After

Width:  |  Height:  |  Size: 307 B

@@ -0,0 +1,9 @@
// Code generated by esbuild. DO NOT EDIT.
"use strict";(()=>{(function(){let t=localStorage.getItem("closed-sections")??"";document.querySelector("html")?.setAttribute("data-closed-sections",t)})();})();
/**
* @license
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
//# sourceMappingURL=storage.min.js.map
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../storage.ts"],
"sourcesContent": ["/**\n * @license\n * Copyright 2023 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n(function () {\n const closedSections = localStorage.getItem(\"closed-sections\") ?? \"\";\n const html = document.querySelector(\"html\");\n html?.setAttribute(\"data-closed-sections\", closedSections);\n})();\n"],
"mappings": ";oBAOC,UAAY,CACX,IAAMA,EAAiB,aAAa,QAAQ,iBAAiB,GAAK,GACrD,SAAS,cAAc,MAAM,GACpC,aAAa,uBAAwBA,CAAc,CAC3D,GAAG",
"names": ["closedSections"]
}
@@ -0,0 +1,12 @@
/**
* @license
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
(function () {
const closedSections = localStorage.getItem("closed-sections") ?? "";
const html = document.querySelector("html");
html?.setAttribute("data-closed-sections", closedSections);
})();
@@ -0,0 +1,89 @@
/*!
* Copyright 2021 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
:root {
/* Colors */
--gray-1: #202224;
--gray-2: #3e4042;
--gray-3: #555759;
--gray-4: #6e7072;
--gray-5: #848688;
--gray-6: #aaacae;
--gray-7: #c6c8ca;
--gray-8: #dcdee0;
--gray-9: #f0f1f2;
--gray-10: #f8f8f8;
--turq-light: #5dc9e2;
--turq-med: #50b7e0;
--turq-dark: #007d9c;
--blue: #bfeaf4;
--blue-light: #f2fafd;
--black: #000;
--green: #3a6e11;
--green-light: #5fda64;
--pink: #c85e7a;
--pink-light: #fdecf1;
--purple: #542c7d;
--slate: #253443; /* Footer background. */
--white: #fff;
--yellow: #fceea5;
--yellow-light: #fff8cc;
/* Color Intents */
--color-brand-primary: var(--turq-dark);
--color-background: var(--white);
--color-background-inverted: var(--slate);
--color-background-accented: var(--gray-10);
--color-background-highlighted: var(--blue);
--color-background-highlighted-link: var(--blue-light);
--color-background-info: var(--gray-9);
--color-background-warning: var(--yellow-light);
--color-background-alert: var(--pink-light);
--color-border: var(--gray-7);
--color-text: var(--gray-1);
--color-text-subtle: var(--gray-4);
--color-text-link: var(--turq-dark);
--color-text-inverted: var(--white);
--color-code-comment: var(--green);
/* Interactive Colors */
--color-input: var(--color-background);
--color-input-text: var(--color-text);
--color-button: var(--turq-dark);
--color-button-disabled: var(--gray-9);
--color-button-text: var(--white);
--color-button-text-disabled: var(--gray-3);
--color-button-inverted: var(--color-background);
--color-button-inverted-disabled: var(--color-background);
--color-button-inverted-text: var(--color-brand-primary);
--color-button-inverted-text-disabled: var(--color-text-subtle);
--color-button-accented: var(--yellow);
--color-button-accented-disabled: var(--gray-9);
--color-button-accented-text: var(--gray-1);
--color-button-accented-text-disabled: var(--gray-3);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-brand-primary: var(--turq-med);
--color-background: var(--gray-1);
--color-background-accented: var(--gray-2);
--color-background-highlighted: var(--gray-2);
--color-background-highlighted-link: var(--gray-2);
--color-background-info: var(--gray-3);
--color-background-warning: var(--yellow);
--color-background-alert: var(--pink);
--color-border: var(--gray-4);
--color-text: var(--gray-9);
--color-text-link: var(--turq-med);
--color-text-subtle: var(--gray-7);
--color-code-comment: var(--green-light);
}
img.go-Icon {
filter: invert(1);
}
}
@@ -0,0 +1,352 @@
/* stylelint-disable */
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
@@ -0,0 +1,41 @@
/*!
* Copyright 2021 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
.go-Tooltip {
border-radius: var(--border-radius);
cursor: pointer;
display: inline-block;
position: relative;
}
.go-Tooltip > summary {
list-style: none;
}
.go-Tooltip > summary::-webkit-details-marker,
.go-Tooltip > summary::marker {
display: none;
}
.go-Tooltip > summary > img {
vertical-align: text-bottom;
}
.go-Tooltip p {
background: var(--color-background) 80%;
border: var(--border);
border-radius: var(--border-radius);
color: var(--color-text);
font-size: 0.75rem;
letter-spacing: 0.0187rem;
line-height: 1rem;
padding: 0.5rem;
position: absolute;
top: 1.5rem;
white-space: normal;
width: 12rem;
z-index: 100;
}
@@ -0,0 +1,20 @@
/**
* @license
* Copyright 2021 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/**
* ToolTipController handles closing tooltips on external clicks.
*/
export class ToolTipController {
constructor(private el: HTMLDetailsElement) {
document.addEventListener("click", (e) => {
const insideTooltip = this.el.contains(e.target as Element);
if (!insideTooltip) {
this.el.removeAttribute("open");
}
});
}
}
@@ -0,0 +1,69 @@
/*!
* Copyright 2021 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
body {
background-color: var(--color-background);
color: var(--color-text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 1rem;
line-height: normal;
}
p {
line-height: 1.4375;
max-width: 75ch;
}
hr {
border: none;
border-bottom: var(--border);
margin: 0;
width: 100%;
}
code,
pre,
textarea.code {
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
font-size: 0.875rem;
line-height: 1.5em;
}
pre,
textarea.code {
background-color: var(--color-background-accented);
border: var(--border);
border-radius: var(--border-radius);
color: var(--color-text);
overflow-x: auto;
padding: 0.625rem;
tab-size: 4;
white-space: pre;
}
button,
input,
select,
textarea {
font: inherit;
}
a,
a:link,
a:visited {
color: var(--color-brand-primary);
text-decoration: none;
}
a:hover {
color: var(--color-brand-primary);
text-decoration: underline;
}
a:hover > * {
text-decoration: underline;
}
@@ -0,0 +1,59 @@
/*!
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
@import url("./_normalize.css");
@import url("./_color.css");
@import url("./_typography.css");
@import url("./_tooltip.css");
:root {
--border: 0.0625rem solid var(--color-border);
--border-radius: 0.25rem;
}
.Breadcrumb {
background-color: var(--color-background-accented);
}
.Breadcrumb ol {
list-style: none;
align-items: center;
padding: 0;
margin: 1.5rem 0;
display: inline-flex;
}
.Breadcrumb li {
display: flex;
font-size: 0.875rem;
}
.Breadcrumb li:not(:last-child):after {
background: url("./arrow-forward.svg") no-repeat;
content: "";
display: block;
height: 1rem;
margin: 0 0.8125rem;
width: 1rem;
text-align: center;
}
.Hero {
background-color: var(--color-background-accented);
padding: 1rem 0;
}
.Hero h1 {
font-size: 2.25rem;
font-weight: normal;
margin: 0;
}
.Container {
margin: 0 0 5rem;
}
.Content {
margin: 0 auto;
max-width: 64rem;
padding: 0 1rem;
}
@@ -0,0 +1,36 @@
<!--
Copyright 2023 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{block "title" .}}{{.Title}}{{end}}</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/base.min.css">
</head>
<body>
{{with .Breadcrumbs}}
<nav class="Breadcrumb">
<div class="Content">
<ol>
{{range .}}
<li>{{if .Link}}<a href="{{.Link}}">{{.Label}}</a>{{else}}{{.Label}}{{end}}</li>
{{end}}
</ol>
</div>
</nav>
{{end}}
<div class="Container">
{{block "content" .}}{{.Content}}{{end}}
</div>
</body>
</html>
{{end}}
@@ -0,0 +1,12 @@
/**
* @license
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
import { ToolTipController } from "./_tooltip";
for (const el of document.querySelectorAll<HTMLDetailsElement>(".js-tooltip")) {
new ToolTipController(el);
}
@@ -0,0 +1,59 @@
/*!
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
@import url("../shared/treenav.css");
/* Fix tooltip background for dark theme */
svg g[aria-label="tip"] g {
fill: var(--color-background);
}
.Chartbrowser-view {
display: flex;
flex-direction: row;
}
.Chartbrowser-index {
flex: 1 1;
padding: 0 1.5rem 0 0;
}
.Chartbrowser-heading {
font-weight: bold;
font-size: 1.25rem;
margin: 0 0 0.5rem 0;
}
.Chartbrowser-index-sticky {
position: sticky;
top: 1rem;
width: 10rem;
}
.Chartbrowser-index-sticky > ul {
position: sticky;
top: 1rem;
margin-top: 0;
}
.Chartbrowser-link {
color: var(--color-text-subtle);
font-size: .875rem;
line-height: 1.5rem;
}
.Chartbrowser-program {
font-weight: normal;
margin: 0 0 1rem 0;
}
.Chartbrowser-program:not(:first-of-type) {
margin-top: 2rem;
}
.Chartbrowser-chart {
background-color: var(--color-background);
border: 1px solid transparent;
margin-bottom: 1rem;
padding: 0.875rem;
box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);
}
.Chartbrowser-chart-name {
text-align: center;
margin: 0;
}
@@ -0,0 +1,54 @@
<!--
Copyright 2024 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<!--
A chart browser is a reusable component for displaying a collection of
charts.
-->
{{define "chartbrowser"}}
<div class="Chartbrowser-view js-Tree">
<div class="Chartbrowser-index">
<nav class="Chartbrowser-index-sticky">
<h3 class="Chartbrowser-heading">Charts</h2>
<ul>
{{range .Charts.Programs}}
{{if .Charts}}
<li class="js-Tree-item" data-heading-id="{{.ID}}">
<a class="Chartbrowser-link" href="#{{.ID}}">{{programName .Name}}</a>
<ul>
{{range .Charts}}
{{with .}}
<li class="js-Tree-item" data-heading-id="{{.ID}}">
<a class="Chartbrowser-link" href="#{{.ID}}">{{chartName .Name}}</a>
</li>
{{end}}
{{end}}
</ul>
</li>
{{end}}
{{end}}
</ul>
</nav>
</div>
<div class="Chartbrowser-content">
{{range .Charts.Programs}}
{{if .Charts}}
{{$progName := programName .Name}}
<h3 id="{{.ID}}" class="Chartbrowser-program js-Tree-heading">{{$progName}}</h3>
{{range .Charts}}
{{with .}}
<div class="Chartbrowser-chart">
<h4 id="{{.ID}}" class="Chartbrowser-chart-name js-Tree-heading">{{$progName}} > {{chartName .Name}}</h4>
<div class="Chart-chart" data-chart-id="{{.ID}}"></div>
</div>
{{end}}
{{end}}
{{end}}
{{end}}
</div>
</div>
{{end}}
@@ -0,0 +1,25 @@
<!--
Copyright 2023 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{block "title" .}}{{.Title}}{{end}}</title>
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/base.min.css">
</head>
<body>
<div class="Container">
<div class="Content">
{{block "content" .}}{{.Content}}{{end}}
</div>
</div>
</body>
</html>
@@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="5" y="2" width="7" height="12">
<path d="M5.06 12.3934L6 13.3334L11.3333 8.00002L6 2.66669L5.06 3.60669L9.44666 8.00002" fill="white"/>
</mask>
<g mask="url(#mask0)">
<rect width="16" height="16" fill="#5F6368"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 386 B

File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
// Code generated by esbuild. DO NOT EDIT.
"use strict";(()=>{var e=class{constructor(i){this.el=i;document.addEventListener("click",o=>{this.el.contains(o.target)||this.el.removeAttribute("open")})}};for(let t of document.querySelectorAll(".js-tooltip"))new e(t);})();
/**
* @license
* Copyright 2021 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/**
* @license
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
//# sourceMappingURL=base.min.js.map
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../_tooltip.ts", "../base.ts"],
"sourcesContent": ["/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * ToolTipController handles closing tooltips on external clicks.\n */\nexport class ToolTipController {\n constructor(private el: HTMLDetailsElement) {\n document.addEventListener(\"click\", (e) => {\n const insideTooltip = this.el.contains(e.target as Element);\n if (!insideTooltip) {\n this.el.removeAttribute(\"open\");\n }\n });\n }\n}\n", "/**\n * @license\n * Copyright 2023 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { ToolTipController } from \"./_tooltip\";\n\nfor (const el of document.querySelectorAll<HTMLDetailsElement>(\".js-tooltip\")) {\n new ToolTipController(el);\n}\n"],
"mappings": ";mBAUO,IAAMA,EAAN,KAAwB,CAC7B,YAAoBC,EAAwB,CAAxB,QAAAA,EAClB,SAAS,iBAAiB,QAAUC,GAAM,CAClB,KAAK,GAAG,SAASA,EAAE,MAAiB,GAExD,KAAK,GAAG,gBAAgB,MAAM,CAElC,CAAC,CACH,CACF,ECVA,QAAWC,KAAM,SAAS,iBAAqC,aAAa,EAC1E,IAAIC,EAAkBD,CAAE",
"names": ["ToolTipController", "el", "e", "el", "ToolTipController"]
}
@@ -0,0 +1,8 @@
/* Code generated by esbuild. DO NOT EDIT. */
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}svg g[aria-label=tip] g{fill:var(--color-background)}.Chartbrowser-view{display:flex;flex-direction:row}.Chartbrowser-index{flex:1 1;padding:0 1.5rem 0 0}.Chartbrowser-heading{font-weight:700;font-size:1.25rem;margin:0 0 .5rem}.Chartbrowser-index-sticky{position:sticky;top:1rem;width:10rem}.Chartbrowser-index-sticky>ul{position:sticky;top:1rem;margin-top:0}.Chartbrowser-link{color:var(--color-text-subtle);font-size:.875rem;line-height:1.5rem}.Chartbrowser-program{font-weight:400;margin:0 0 1rem}.Chartbrowser-program:not(:first-of-type){margin-top:2rem}.Chartbrowser-chart{background-color:var(--color-background);border:1px solid transparent;margin-bottom:1rem;padding:.875rem;box-shadow:0 1px 2px #3c40434d,0 1px 3px 1px #3c404326}.Chartbrowser-chart-name{text-align:center;margin:0}
/*!
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/*# sourceMappingURL=chartbrowser.min.css.map */
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../treenav.css", "../chartbrowser.css"],
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/treenav.css\");\n\n/* Fix tooltip background for dark theme */\nsvg g[aria-label=\"tip\"] g {\n fill: var(--color-background);\n}\n\n.Chartbrowser-view {\n display: flex;\n flex-direction: row;\n}\n.Chartbrowser-index {\n flex: 1 1;\n padding: 0 1.5rem 0 0;\n}\n.Chartbrowser-heading {\n font-weight: bold;\n font-size: 1.25rem;\n margin: 0 0 0.5rem 0;\n}\n.Chartbrowser-index-sticky {\n position: sticky;\n top: 1rem;\n width: 10rem;\n}\n.Chartbrowser-index-sticky > ul {\n position: sticky;\n top: 1rem;\n margin-top: 0;\n}\n.Chartbrowser-link {\n color: var(--color-text-subtle);\n font-size: .875rem;\n line-height: 1.5rem;\n}\n.Chartbrowser-program {\n font-weight: normal;\n margin: 0 0 1rem 0;\n}\n.Chartbrowser-program:not(:first-of-type) {\n margin-top: 2rem;\n}\n.Chartbrowser-chart {\n background-color: var(--color-background);\n border: 1px solid transparent;\n margin-bottom: 1rem;\n padding: 0.875rem;\n box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);\n}\n.Chartbrowser-chart-name {\n text-align: center;\n margin: 0;\n}\n"],
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE,wBCvCF,wBACE,6BAGF,mBACE,aACA,mBAEF,oBACE,SAlBF,qBAqBA,sBACE,gBACA,kBAvBF,iBA0BA,2BACE,gBACA,SACA,YAEF,8BACE,gBACA,SACA,aAEF,mBACE,+BACA,kBACA,mBAEF,sBACE,gBA1CF,gBA6CA,0CACE,gBAEF,oBACE,yCACA,6BACA,mBAnDF,gBAqDE,uDAEF,yBACE,kBAxDF",
"names": []
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1,8 @@
/* Code generated by esbuild. DO NOT EDIT. */
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}
/*!
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/*# sourceMappingURL=treenav.min.css.map */
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../treenav.css"],
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n"],
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE",
"names": []
}
@@ -0,0 +1,9 @@
// Code generated by esbuild. DO NOT EDIT.
"use strict";(()=>{function h(r){let n=r.querySelectorAll(".js-Tree-heading"),s=()=>{let o=[];for(let e of n){let t=e.getBoundingClientRect();t.height&&t.top<80&&o.unshift(e)}o.length==0&&n[0]instanceof HTMLHeadingElement&&(o=[n[0]]);let l=1/0,a=[];for(let e of o){let t=Number(e.tagName[1]);t<l&&(l=t,a.push(e))}let d=r.querySelectorAll(".js-Tree-item");for(let e of d){let t=e.dataset.headingId,c=!1,f=!1;for(let u of a)if(u.id===t){u===a[0]?c=!0:f=!0;break}e.setAttribute("aria-selected",c?"true":"false"),e.setAttribute("aria-expanded",f?"true":"false")}},i=new IntersectionObserver(m(s,20));for(let o of n)i.observe(o)}function m(r,n){let s;return(...i)=>{clearTimeout(s),s=setTimeout(()=>r(...i),n)}}})();
/**
* @license
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
//# sourceMappingURL=treenav.min.js.map
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../treenav.ts"],
"sourcesContent": ["/**\n * @license\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * A treeNavController adds dynamic expansion and selection of index list\n * elements based on scroll position.\n *\n * Use it as follows:\n * - Add the .js-Tree class to a parent element of your index and content.\n * - Add the .js-Tree-item class to <li> elements of your index.\n * - Add the .js-Tree-heading class to <hN> heading elements of your content.\n *\n * Then, when you scroll content, the 'aria-selected' and 'aria-expanded'\n * attributes of your tree items will be set according to the current content\n * scroll position. The included treenav.css implements styling to expand and\n * highlight index elements according to these attributes.\n */\nexport function treeNavController(el: HTMLElement) {\n const headings = el.querySelectorAll<HTMLHeadingElement>(\".js-Tree-heading\");\n const callback = () => {\n // Collect heading elements above the scroll position.\n let above: HTMLHeadingElement[] = [];\n for (const h of headings) {\n const rect = h.getBoundingClientRect();\n if (rect.height && rect.top < 80) {\n above.unshift(h);\n }\n }\n // Highlight the first heading even if we're not yet scrolled below it.\n if (above.length == 0 && headings[0] instanceof HTMLHeadingElement) {\n above = [headings[0]];\n }\n // Collect the set of heading levels we're immediately below, at most one\n // per heading level, by decresing level.\n // e.g. [<h3 element>, <h2 element>, <h1 element>]\n let threshold = Infinity;\n const active: HTMLHeadingElement[] = [];\n for (const h of above) {\n const level = Number(h.tagName[1]);\n if (level < threshold) {\n threshold = level;\n active.push(h);\n }\n }\n // Update aria-selected and aria-expanded for all items, per the current\n // position.\n const navItems = el.querySelectorAll<HTMLElement>(\".js-Tree-item\");\n for (const item of navItems) {\n const headingId = item.dataset[\"headingId\"];\n let selected = false,\n expanded = false;\n for (const h of active) {\n if (h.id === headingId) {\n if (h === active[0]) {\n selected = true;\n } else {\n expanded = true;\n }\n break;\n }\n }\n item.setAttribute(\"aria-selected\", selected ? \"true\" : \"false\");\n item.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n };\n\n // Update on changes to viewport intersection, defensively debouncing to\n // guard against performance issues.\n const observer = new IntersectionObserver(debounce(callback, 20));\n for (const h of headings) {\n observer.observe(h);\n }\n}\n\nexport function debounce<T extends (...args: unknown[]) => unknown>(\n callback: T,\n wait: number\n) {\n let timeout: number;\n return (...args: unknown[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => callback(...args), wait);\n };\n}\n"],
"mappings": ";mBAqBO,SAASA,EAAkBC,EAAiB,CACjD,IAAMC,EAAWD,EAAG,iBAAqC,kBAAkB,EACrEE,EAAW,IAAM,CAErB,IAAIC,EAA8B,CAAC,EACnC,QAAWC,KAAKH,EAAU,CACxB,IAAMI,EAAOD,EAAE,sBAAsB,EACjCC,EAAK,QAAUA,EAAK,IAAM,IAC5BF,EAAM,QAAQC,CAAC,EAIfD,EAAM,QAAU,GAAKF,EAAS,CAAC,YAAa,qBAC9CE,EAAQ,CAACF,EAAS,CAAC,CAAC,GAKtB,IAAIK,EAAY,IACVC,EAA+B,CAAC,EACtC,QAAWH,KAAKD,EAAO,CACrB,IAAMK,EAAQ,OAAOJ,EAAE,QAAQ,CAAC,CAAC,EAC7BI,EAAQF,IACVA,EAAYE,EACZD,EAAO,KAAKH,CAAC,GAKjB,IAAMK,EAAWT,EAAG,iBAA8B,eAAe,EACjE,QAAWU,KAAQD,EAAU,CAC3B,IAAME,EAAYD,EAAK,QAAQ,UAC3BE,EAAW,GACbC,EAAW,GACb,QAAWT,KAAKG,EACd,GAAIH,EAAE,KAAOO,EAAW,CAClBP,IAAMG,EAAO,CAAC,EAChBK,EAAW,GAEXC,EAAW,GAEb,MAGJH,EAAK,aAAa,gBAAiBE,EAAW,OAAS,OAAO,EAC9DF,EAAK,aAAa,gBAAiBG,EAAW,OAAS,OAAO,EAElE,EAIMC,EAAW,IAAI,qBAAqBC,EAASb,EAAU,EAAE,CAAC,EAChE,QAAWE,KAAKH,EACda,EAAS,QAAQV,CAAC,CAEtB,CAEO,SAASW,EACdb,EACAc,EACA,CACA,IAAIC,EACJ,MAAO,IAAIC,IAAoB,CAC7B,aAAaD,CAAO,EACpBA,EAAU,WAAW,IAAMf,EAAS,GAAGgB,CAAI,EAAGF,CAAI,CACpD,CACF",
"names": ["treeNavController", "el", "headings", "callback", "above", "h", "rect", "threshold", "active", "level", "navItems", "item", "headingId", "selected", "expanded", "observer", "debounce", "wait", "timeout", "args"]
}
@@ -0,0 +1,51 @@
/*!
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
.js-Tree ul {
list-style: none;
padding-left: 0;
}
.js-Tree-item ul {
display: none;
}
.js-Tree-item {
overflow: hidden;
text-overflow: ellipsis;
padding: 0.125rem 0 0.125rem 0;
}
.js-Tree-item[aria-expanded='true'] ul {
display: block;
}
.js-Tree-item .js-Tree-item {
position: relative;
padding-left: 1.25rem;
}
.js-Tree-item .js-Tree-item[aria-selected='true']:before {
background-color: var(--color-brand-primary);
border-radius: 50%;
content: "";
display: block;
height: .3125rem;
left: .4688rem;
position: absolute;
top: .75rem;
width: .3125rem;
}
.js-Tree-item>a {
color: var(--color-text-subtle);
font-size: .875rem;
}
.js-Tree-item[aria-selected='true']>a {
color: var(--color-text);
}
@@ -0,0 +1,88 @@
/**
* @license
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/**
* A treeNavController adds dynamic expansion and selection of index list
* elements based on scroll position.
*
* Use it as follows:
* - Add the .js-Tree class to a parent element of your index and content.
* - Add the .js-Tree-item class to <li> elements of your index.
* - Add the .js-Tree-heading class to <hN> heading elements of your content.
*
* Then, when you scroll content, the 'aria-selected' and 'aria-expanded'
* attributes of your tree items will be set according to the current content
* scroll position. The included treenav.css implements styling to expand and
* highlight index elements according to these attributes.
*/
export function treeNavController(el: HTMLElement) {
const headings = el.querySelectorAll<HTMLHeadingElement>(".js-Tree-heading");
const callback = () => {
// Collect heading elements above the scroll position.
let above: HTMLHeadingElement[] = [];
for (const h of headings) {
const rect = h.getBoundingClientRect();
if (rect.height && rect.top < 80) {
above.unshift(h);
}
}
// Highlight the first heading even if we're not yet scrolled below it.
if (above.length == 0 && headings[0] instanceof HTMLHeadingElement) {
above = [headings[0]];
}
// Collect the set of heading levels we're immediately below, at most one
// per heading level, by decresing level.
// e.g. [<h3 element>, <h2 element>, <h1 element>]
let threshold = Infinity;
const active: HTMLHeadingElement[] = [];
for (const h of above) {
const level = Number(h.tagName[1]);
if (level < threshold) {
threshold = level;
active.push(h);
}
}
// Update aria-selected and aria-expanded for all items, per the current
// position.
const navItems = el.querySelectorAll<HTMLElement>(".js-Tree-item");
for (const item of navItems) {
const headingId = item.dataset["headingId"];
let selected = false,
expanded = false;
for (const h of active) {
if (h.id === headingId) {
if (h === active[0]) {
selected = true;
} else {
expanded = true;
}
break;
}
}
item.setAttribute("aria-selected", selected ? "true" : "false");
item.setAttribute("aria-expanded", expanded ? "true" : "false");
}
};
// Update on changes to viewport intersection, defensively debouncing to
// guard against performance issues.
const observer = new IntersectionObserver(debounce(callback, 20));
for (const h of headings) {
observer.observe(h);
}
}
export function debounce<T extends (...args: unknown[]) => unknown>(
callback: T,
wait: number
) {
let timeout: number;
return (...args: unknown[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => callback(...args), wait);
};
}
@@ -0,0 +1,35 @@
<!--
Copyright 2024 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{template "base" .}}
{{define "title"}}Go Telemetry / Daily Charts{{end}}
{{define "content"}}
<main id="main">
<section>
<div class="Hero">
<div class="Content">
<h1>Daily Charts</h1>
<p>View charts for telemetry data.</p>
</div>
</div>
</section>
<section>
<div class="Content">
<ul style="column-count: auto; column-width: 10rem">
{{range .}}
<li><a href="/charts/{{.}}">{{.}}</a></li>
{{end}}
</ul>
</div>
</section>
</main>
{{end}}
@@ -0,0 +1,12 @@
/*!
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
@import url("../shared/chartbrowser.css");
.Charts {
margin-top: 1.5rem;
}
@@ -0,0 +1,37 @@
<!--
Copyright 2023 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{template "base" .}}
{{define "title"}}Go Telemetry / {{index .Charts.DateRange 1}}{{end}}
{{define "content"}}
<link rel="stylesheet" href="/static/charts.min.css">
<main id="main">
<section>
<div class="Hero">
<div class="Content">
<h1>{{.ChartTitle}}</h1>
<p>Generated from {{.Charts.NumReports}} reports.</p>
</div>
</div>
</section>
<section>
<div class="Content">
<div class="Charts">
{{template "chartbrowser" .}}
</div>
</div>
</section>
<script>
window.Page = {{.}};
</script>
<script src="/static/charts.min.js"></script>
{{end}}
@@ -0,0 +1,169 @@
/**
* @license
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
interface Page {
Charts: ChartData | null;
}
interface ChartData {
Programs: Program[] | null;
}
interface Program {
ID: string;
Name: string;
Charts: Chart[] | null;
}
interface Chart {
ID: string;
Name: string;
Type: string;
Data: Datum[] | null;
}
interface Datum {
Key: string;
Value: number;
}
declare const Page: Page;
import * as d3 from "d3";
import * as Plot from "@observablehq/plot";
import { treeNavController } from "../shared/treenav";
for (const program of Page.Charts?.Programs || []) {
for (const counter of program?.Charts || []) {
switch (counter.Type) {
case "partition":
document
.querySelector(`[data-chart-id="${counter.ID}"]`)
?.append(partition(counter));
break;
case "histogram":
document
.querySelector(`[data-chart-id="${counter.ID}"]`)
?.append(histogram(counter));
break;
default:
console.error("unknown chart type");
break;
}
}
}
for (const el of document.querySelectorAll<HTMLElement>(".js-Tree")) {
treeNavController(el);
}
function partition({ Data, Name }: Chart) {
Data ??= [];
const max = Data.map((d) => d.Value).reduce((a, b) => Math.max(a, b), 0);
return Plot.plot({
color: {
type: "categorical",
scheme: "set2",
},
nice: true,
x: {
label: Name,
labelOffset: Number.MAX_SAFE_INTEGER,
tickRotate: 45,
domain: Data.map((d) => d.Key),
},
y: {
label: "Reports", // currently, partition charts count the number of reports, not counter totals.
domain: [0, max + 1], // adjust domain to prevent rendering issues, especially with all-zero data.
},
width: 1024,
style: {
overflow: "visible",
background: "transparent",
marginBottom: "3rem",
fontSize: "0.8rem",
marginTop: "1rem",
},
insetTop: 20, // leave enough space between the axis label and marks
marks: [
Plot.barY(Data, {
tip: true,
fill: (d) => (isNaN(Number(d.Key)) ? d.Key : Number(d.Key)),
x: (d) => d.Key,
y: (d) => d.Value,
}),
Plot.frame(),
],
});
}
function histogram({ Data }: Chart) {
Data ??= [];
const n = 3; // number of facet columns
const fixKey = (k: string) => (isNaN(Number(k)) ? k : Number(k));
const keys = Array.from(d3.union(Data.map((d) => fixKey(d.Key))));
const index = new Map(keys.map((key, i) => [key, i]));
const fx = (key: string | number) => (index.get(key) ?? 0) % n;
const fy = (key: string | number) => Math.floor((index.get(key) ?? 0) / n);
return Plot.plot({
marginLeft: 60,
width: 1024,
grid: true,
nice: true,
x: {
label: "Distribution",
},
color: {
type: "ordinal",
legend: true,
scheme: "Spectral",
label: "Counter",
},
y: {
insetTop: 16,
domain: [0, 1],
},
fx: {
ticks: [],
},
fy: {
ticks: [],
},
style: "background:transparent;",
marks: [
Plot.barY(
Data,
Plot.binX(
{ y: "proportion-facet", x: "x1", interval: 0.1, cumulative: 1 },
{
tip: true,
fill: (d: Datum) => fixKey(d.Key),
x: (d: Datum) => d.Value,
fx: (d: Datum) => fx(fixKey(d.Key)),
fy: (d: Datum) => fy(fixKey(d.Key)),
}
)
),
Plot.text(keys, {
frameAnchor: "top",
dy: 3,
fx,
fy,
}),
Plot.axisX({ anchor: "bottom", tickSpacing: 35 }),
Plot.axisX({ anchor: "top", tickSpacing: 35 }),
Plot.frame(),
],
});
}
export {};
@@ -0,0 +1,40 @@
<!--
Copyright 2023 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{template "base" .}}
{{define "title"}}Go Telemetry Config{{end}}
{{define "content"}}
<main id="main">
<div class="Content">
<section class="Chart Config">
<h2 id="config">Chart Config</h2>
<p>
The chart config contains the list of approved charts to display on
telemetry.go.dev. The chart config format is documented by the
<a href="https://pkg.go.dev/golang.org/x/telemetry/internal/chartconfig">
<code>chartconfig</code>
</a> package documentation.
</p>
<pre style="max-height: 100rem">{{.ChartConfig}}</pre>
</section>
<section class="Upload Config">
<h2 id="config">Upload Config</h2>
<p>
The upload config contains the list of active counters for each program
and allowed report metadata. This is generated from the chart config
above.
</p>
<label>
Version: {{.Version}}
</label>
<pre style="max-height: 100rem">{{.UploadConfig}}</pre>
</section>
</div>
</main>
{{end}}
@@ -0,0 +1,36 @@
<!--
Copyright 2024 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{template "base" .}}
{{define "title"}}Go Telemetry / Data{{end}}
{{define "content"}}
<main id="main">
<section>
<div class="Hero">
<div class="Content">
<h1>Merged daily reports</h1>
<p>Download raw telemetry data.</p>
</div>
</div>
</section>
<section>
<div class="Content">
<ul style="margin-top: 1.5rem; column-count: auto; column-width: 10rem">
{{$url := .BucketURL}}
{{range .Dates}}
<li><a href="{{$url}}/{{.}}.json">{{.}}</a></li>
{{end}}
</ul>
</div>
</section>
</main>
{{end}}
@@ -0,0 +1,46 @@
/*!
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
@import url("../shared/chartbrowser.css");
p {
/* Reset from _typography.css */
max-width: none;
}
.Charts {
margin-top: 1rem;
}
.Centered {
display: flex;
justify-content: center;
}
.Charts-heading {
margin: 2.5rem 0 1.5rem;
}
.Charts-heading h2 {
font-weight: normal;
margin: 0;
}
.Charts-heading a {
border: var(--border);
padding: 0.6rem;
border-radius: 0.5rem;
background-color: var(--color-background-highlighted-link);
}
.Charts-heading ul {
margin: 0.5rem 0 0 0;
list-style: none;
display: inline-flex;
padding: 0;
}
.Charts-heading li {
list-style: none;
display: inline-flex;
}
.Charts-heading li:not(:last-child) {
margin-right: 1rem;
}
@@ -0,0 +1,61 @@
<!--
Copyright 2023 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
{{template "base" .}}
{{define "title"}}Go Telemetry{{end}}
{{define "content"}}
<link rel="stylesheet" href="/static/index.min.css">
<main id="main">
<section>
<div class="Hero">
<div class="Content">
<h1>Go Telemetry &#128202;</h1>
<p>
<em>Go Telemetry</em> is a way for Go toolchain programs to collect
data about their performance and usage. Uploaded data is used to help
improve the Go toolchain and related tools. Go Telemetry is not built
into users' binaries. Learn more about Go telemetry at
<a href="https://go.dev/doc/telemetry">go.dev/doc/telemetry</a>.
</p>
<p>
Users who have opted in will upload an approved subset of telemetry
data approximately once a week. This subset is determined by the current
<a href="/config">upload configuration</a>.
</p>
<p>
For privacy information about this service, see
<a href="/privacy">telemetry.go.dev/privacy</a>.
</p>
</div>
</div>
</section>
<section class="Charts">
<div class="Content">
<div class="Charts-heading">
<h2>{{.ChartTitle}}</h2>
<ul>
<li><a href="/charts/">All charts</a></li>
<li><a href="/data/">All raw data</a></li>
</ul>
</div>
{{template "chartbrowser" .}}
</div>
</section>
</main>
<script>
window.Page = {{.}};
</script>
<script src="/static/charts.min.js"></script>
{{end}}
@@ -0,0 +1,88 @@
---
Title: Go Telemetry Privacy Policy
Layout: privacy.html
---
# Privacy Policy
_Last updated: January 24, 2024_
Go Telemetry is a way for Go toolchain programs to collect data about their
performance and usage. This data can help developers improve the language and
tools.
## What Go Telemetry Records {#collection}
Go toolchain programs, such as the `go` command and `gopls`, record certain information
about their own execution. This data is stored in local files on your computer,
specifically in the [`os.UserConfigDir()/go/telemetry/local`](https://pkg.go.dev/os#UserConfigDir) directory.
Here is what these files contain:
* Event counters: Information about how Go toolchain programs
are used.
* Stack traces: Details about program execution for troubleshooting.
* Basic system information: Your operating system, CPU architecture, and name and version of the Go tool being executed.
Importantly, these files do not contain personal or other
identifying information about you or your system.
## Data Privacy {#data-privacy}
By default, the data collected by Go Telemetry is kept only locally on your computer.
It is not shared with anyone unless you explicitly decide to enable Go Telemetry.
You can do this by running the command [`gotelemetry on`](#command) or using a command
in your integrated development environment (IDE).
Once enabled, Go Telemetry may decide once a week to upload reports to a Google
server. A local copy of the uploaded reports is kept in the
[`os.UserConfigDir()/go/telemetry/remote`](https://pkg.go.dev/os#UserConfigDir) directory on the user's machine.
These reports include only approved counters and are collected in
accordance with the Google Privacy Policy, which you can find
at [Google Privacy Policy](https://policies.google.com/privacy).
The uploaded reports are also made available as part of a public dataset at
[telemetry.go.dev](https://telemetry.go.dev). Developers working on Go,
both inside and outside of Google, use this dataset to understand
how the Go toolchain is used and if it is performing as expected.
## Using the `gotelemetry` Command Line Tool {#command}
To manage Go Telemetry, you can use the `gotelemetry` command line tool.
go install golang.org/x/telemetry/cmd/gotelemetry@latest
Here are some useful commands:
* `gotelemetry on`: Upload Go Telemetry data weekly.
* `gotelemetry off`: Do not upload Go Telemetry data.
* `gotelemetry view`: View locally collected telemetry data.
* `gotelemetry clear`: Clear locally collected telemetry data at any time.
For the complete usage documentation of the gotelemetry command line tool, visit
[golang.org/x/telemetry/cmd/gotelemetry](https://golang.org/x/telemetry/cmd/gotelemetry).
## Approved Counters {#config}
Go Telemetry only uploads counters that have been approved through the [public proposal process](https://github.com/orgs/golang/projects/29).
You can find the set of approved counters as a Go module at
[golang.org/x/telemetry/config](https://go.googlesource.com/telemetry/+/refs/heads/master/config/config.json) and the [current config in use](https://telemetry.go.dev/config).
## IDE Integration {#integration}
If you're using an integrated development environment (IDE) like Visual Studio
Code, versions
[`v0.14.0`](https://github.com/golang/tools/releases/tag/gopls%2Fv0.14.0) and
later of the Go language server [gopls](https://go.dev/s/gopls) collect
telemetry data. As described above, data is only uploaded after you have opted
in, either by using the command [`gotelemetry on`](#command) as described above
or by accepting a dialog in the IDE.
You can always opt out of uploading at any time by using the
[`gotelemetry local`](#command) or [`gotelemetry off`](#command) commands.
By sharing performance statistics, usage information, and crash reports with Go Telemetry,
you can help improve the Go programming language and its tools while also ensuring
your data privacy.
@@ -0,0 +1,13 @@
/* Code generated by esbuild. DO NOT EDIT. */
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}svg g[aria-label=tip] g{fill:var(--color-background)}.Chartbrowser-view{display:flex;flex-direction:row}.Chartbrowser-index{flex:1 1;padding:0 1.5rem 0 0}.Chartbrowser-heading{font-weight:700;font-size:1.25rem;margin:0 0 .5rem}.Chartbrowser-index-sticky{position:sticky;top:1rem;width:10rem}.Chartbrowser-index-sticky>ul{position:sticky;top:1rem;margin-top:0}.Chartbrowser-link{color:var(--color-text-subtle);font-size:.875rem;line-height:1.5rem}.Chartbrowser-program{font-weight:400;margin:0 0 1rem}.Chartbrowser-program:not(:first-of-type){margin-top:2rem}.Chartbrowser-chart{background-color:var(--color-background);border:1px solid transparent;margin-bottom:1rem;padding:.875rem;box-shadow:0 1px 2px #3c40434d,0 1px 3px 1px #3c404326}.Chartbrowser-chart-name{text-align:center;margin:0}.Charts{margin-top:1.5rem}
/*!
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/*!
* Copyright 2023 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/*# sourceMappingURL=charts.min.css.map */
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../shared/treenav.css", "../../shared/chartbrowser.css", "../charts.css"],
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/treenav.css\");\n\n/* Fix tooltip background for dark theme */\nsvg g[aria-label=\"tip\"] g {\n fill: var(--color-background);\n}\n\n.Chartbrowser-view {\n display: flex;\n flex-direction: row;\n}\n.Chartbrowser-index {\n flex: 1 1;\n padding: 0 1.5rem 0 0;\n}\n.Chartbrowser-heading {\n font-weight: bold;\n font-size: 1.25rem;\n margin: 0 0 0.5rem 0;\n}\n.Chartbrowser-index-sticky {\n position: sticky;\n top: 1rem;\n width: 10rem;\n}\n.Chartbrowser-index-sticky > ul {\n position: sticky;\n top: 1rem;\n margin-top: 0;\n}\n.Chartbrowser-link {\n color: var(--color-text-subtle);\n font-size: .875rem;\n line-height: 1.5rem;\n}\n.Chartbrowser-program {\n font-weight: normal;\n margin: 0 0 1rem 0;\n}\n.Chartbrowser-program:not(:first-of-type) {\n margin-top: 2rem;\n}\n.Chartbrowser-chart {\n background-color: var(--color-background);\n border: 1px solid transparent;\n margin-bottom: 1rem;\n padding: 0.875rem;\n box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);\n}\n.Chartbrowser-chart-name {\n text-align: center;\n margin: 0;\n}\n", "/*!\n * Copyright 2023 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n\n@import url(\"../shared/chartbrowser.css\");\n\n.Charts {\n margin-top: 1.5rem;\n}\n"],
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE,wBCvCF,wBACE,6BAGF,mBACE,aACA,mBAEF,oBACE,SAlBF,qBAqBA,sBACE,gBACA,kBAvBF,iBA0BA,2BACE,gBACA,SACA,YAEF,8BACE,gBACA,SACA,aAEF,mBACE,+BACA,kBACA,mBAEF,sBACE,gBA1CF,gBA6CA,0CACE,gBAEF,oBACE,yCACA,6BACA,mBAnDF,gBAqDE,uDAEF,yBACE,kBAxDF,SCSA,QACE",
"names": []
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
/* Code generated by esbuild. DO NOT EDIT. */
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}svg g[aria-label=tip] g{fill:var(--color-background)}.Chartbrowser-view{display:flex;flex-direction:row}.Chartbrowser-index{flex:1 1;padding:0 1.5rem 0 0}.Chartbrowser-heading{font-weight:700;font-size:1.25rem;margin:0 0 .5rem}.Chartbrowser-index-sticky{position:sticky;top:1rem;width:10rem}.Chartbrowser-index-sticky>ul{position:sticky;top:1rem;margin-top:0}.Chartbrowser-link{color:var(--color-text-subtle);font-size:.875rem;line-height:1.5rem}.Chartbrowser-program{font-weight:400;margin:0 0 1rem}.Chartbrowser-program:not(:first-of-type){margin-top:2rem}.Chartbrowser-chart{background-color:var(--color-background);border:1px solid transparent;margin-bottom:1rem;padding:.875rem;box-shadow:0 1px 2px #3c40434d,0 1px 3px 1px #3c404326}.Chartbrowser-chart-name{text-align:center;margin:0}p{max-width:none}.Charts{margin-top:1rem}.Centered{display:flex;justify-content:center}.Charts-heading{margin:2.5rem 0 1.5rem}.Charts-heading h2{font-weight:400;margin:0}.Charts-heading a{border:var(--border);padding:.6rem;border-radius:.5rem;background-color:var(--color-background-highlighted-link)}.Charts-heading ul{margin:.5rem 0 0;list-style:none;display:inline-flex;padding:0}.Charts-heading li{list-style:none;display:inline-flex}.Charts-heading li:not(:last-child){margin-right:1rem}
/*!
* Copyright 2024 The Go Authors. All rights reserved.
* Use of this source code is governed by a BSD-style
* license that can be found in the LICENSE file.
*/
/*# sourceMappingURL=index.min.css.map */
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../shared/treenav.css", "../../shared/chartbrowser.css", "../index.css"],
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/treenav.css\");\n\n/* Fix tooltip background for dark theme */\nsvg g[aria-label=\"tip\"] g {\n fill: var(--color-background);\n}\n\n.Chartbrowser-view {\n display: flex;\n flex-direction: row;\n}\n.Chartbrowser-index {\n flex: 1 1;\n padding: 0 1.5rem 0 0;\n}\n.Chartbrowser-heading {\n font-weight: bold;\n font-size: 1.25rem;\n margin: 0 0 0.5rem 0;\n}\n.Chartbrowser-index-sticky {\n position: sticky;\n top: 1rem;\n width: 10rem;\n}\n.Chartbrowser-index-sticky > ul {\n position: sticky;\n top: 1rem;\n margin-top: 0;\n}\n.Chartbrowser-link {\n color: var(--color-text-subtle);\n font-size: .875rem;\n line-height: 1.5rem;\n}\n.Chartbrowser-program {\n font-weight: normal;\n margin: 0 0 1rem 0;\n}\n.Chartbrowser-program:not(:first-of-type) {\n margin-top: 2rem;\n}\n.Chartbrowser-chart {\n background-color: var(--color-background);\n border: 1px solid transparent;\n margin-bottom: 1rem;\n padding: 0.875rem;\n box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);\n}\n.Chartbrowser-chart-name {\n text-align: center;\n margin: 0;\n}\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/chartbrowser.css\");\n\np {\n /* Reset from _typography.css */\n max-width: none;\n}\n\n.Charts {\n margin-top: 1rem;\n}\n.Centered {\n display: flex;\n justify-content: center;\n}\n.Charts-heading {\n margin: 2.5rem 0 1.5rem;\n}\n.Charts-heading h2 {\n font-weight: normal;\n margin: 0;\n}\n.Charts-heading a {\n border: var(--border);\n padding: 0.6rem;\n border-radius: 0.5rem;\n background-color: var(--color-background-highlighted-link);\n}\n.Charts-heading ul {\n margin: 0.5rem 0 0 0;\n list-style: none;\n display: inline-flex;\n padding: 0;\n}\n.Charts-heading li {\n list-style: none;\n display: inline-flex;\n}\n.Charts-heading li:not(:last-child) {\n margin-right: 1rem;\n}\n"],
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE,wBCvCF,wBACE,6BAGF,mBACE,aACA,mBAEF,oBACE,SAlBF,qBAqBA,sBACE,gBACA,kBAvBF,iBA0BA,2BACE,gBACA,SACA,YAEF,8BACE,gBACA,SACA,aAEF,mBACE,+BACA,kBACA,mBAEF,sBACE,gBA1CF,gBA6CA,0CACE,gBAEF,oBACE,yCACA,6BACA,mBAnDF,gBAqDE,uDAEF,yBACE,kBAxDF,SCQA,EAEE,eAGF,QACE,gBAEF,UACE,aACA,uBAEF,gBApBA,uBAuBA,mBACE,gBAxBF,SA2BA,kBACI,qBA5BJ,kCA+BI,0DAEJ,mBAjCA,iBAmCE,gBACA,oBApCF,UAuCA,mBACE,gBACA,oBAEF,oCACE",
"names": []
}

Some files were not shown because too many files have changed in this diff Show More