whatcanGOwrong
This commit is contained in:
+30
@@ -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
|
||||
+82
@@ -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)
|
||||
}
|
||||
}
|
||||
+68
@@ -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
|
||||
}
|
||||
}
|
||||
+142
@@ -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
|
||||
}
|
||||
+18
@@ -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
|
||||
+675
@@ -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
|
||||
}
|
||||
+156
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+395
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user