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,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)
}