internal/telemetry: encapsulate the telemetry directory
In several places throughout tests, we set all of telemetry.{LocalDir,
UploadDir, Mode} according to the standard schema ("local/", "upload/",
and "mode").
In the interest of reducing or eliminating global state to improve
testability, introduce a telemetry.Dir type which encapsulates the
telemetry directory layout. The default layout is accessed through
telemetry.Default. This makes it easier for future components to close
over or pass around a single piece of state (the Dir). In subsequent
CLs, this will be used to make uploading more testable.
For golang/go#66003
Change-Id: I31db8df20f133d834219ff17c2fe2d4f9e446b5d
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/575059
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Peter Weinberger <pjw@google.com>
diff --git a/cmd/gotelemetry/internal/csv/csv.go b/cmd/gotelemetry/internal/csv/csv.go
index b20357c..bba140b 100644
--- a/cmd/gotelemetry/internal/csv/csv.go
+++ b/cmd/gotelemetry/internal/csv/csv.go
@@ -32,7 +32,7 @@
}
func Csv() {
- files, err := readdir(telemetry.LocalDir, nil)
+ files, err := readdir(telemetry.Default.LocalDir(), nil)
if err != nil {
log.Fatal(err)
}
diff --git a/cmd/gotelemetry/internal/view/view.go b/cmd/gotelemetry/internal/view/view.go
index 25232d7..b781b3e 100644
--- a/cmd/gotelemetry/internal/view/view.go
+++ b/cmd/gotelemetry/internal/view/view.go
@@ -114,16 +114,17 @@
if err != nil {
return err
}
- if _, err := os.Stat(telemetry.LocalDir); err != nil {
+ 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.LocalDir)
+There is nothing to report.`, telemetry.Default.LocalDir())
}
- reports, err := reports(telemetry.LocalDir, cfg)
+ reports, err := reports(localDir, cfg)
if err != nil {
return err
}
- files, err := files(telemetry.LocalDir, cfg)
+ files, err := files(localDir, cfg)
if err != nil {
return err
}
diff --git a/cmd/gotelemetry/main.go b/cmd/gotelemetry/main.go
index 6538d97..c86213f 100644
--- a/cmd/gotelemetry/main.go
+++ b/cmd/gotelemetry/main.go
@@ -213,10 +213,10 @@
}
func runOn(_ []string) {
- if old, _ := telemetry.Mode(); old == "on" {
+ if old, _ := telemetry.Default.Mode(); old == "on" {
return
}
- if err := telemetry.SetMode("on"); err != nil {
+ 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
@@ -236,19 +236,19 @@
}
func runLocal(_ []string) {
- if old, _ := telemetry.Mode(); old == "local" {
+ if old, _ := telemetry.Default.Mode(); old == "local" {
return
}
- if err := telemetry.SetMode("local"); err != nil {
+ 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.Mode(); old == "off" {
+ if old, _ := telemetry.Default.Mode(); old == "off" {
return
}
- if err := telemetry.SetMode("off"); err != nil {
+ if err := telemetry.Default.SetMode("off"); err != nil {
failf("Failed to disable telemetry: %v", err)
}
}
@@ -258,12 +258,12 @@
}
func runEnv(_ []string) {
- m, t := telemetry.Mode()
+ m, t := telemetry.Default.Mode()
fmt.Printf("mode: %s %s\n", m, t)
fmt.Println()
- fmt.Println("modefile:", telemetry.ModeFile)
- fmt.Println("localdir:", telemetry.LocalDir)
- fmt.Println("uploaddir:", telemetry.UploadDir)
+ fmt.Println("modefile:", telemetry.Default.ModeFile())
+ fmt.Println("localdir:", telemetry.Default.LocalDir())
+ fmt.Println("uploaddir:", telemetry.Default.UploadDir())
}
func runClean(_ []string) {
@@ -271,8 +271,8 @@
// 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.LocalDir: {"." + counter.FileVersion + ".count", ".json"},
- telemetry.UploadDir: {".json"},
+ telemetry.Default.LocalDir(): {"." + counter.FileVersion + ".count", ".json"},
+ telemetry.Default.UploadDir(): {".json"},
} {
entries, err := os.ReadDir(dir)
if err != nil {
@@ -307,7 +307,7 @@
func runDump(args []string) {
if len(args) == 0 {
- localdir := telemetry.LocalDir
+ localdir := telemetry.Default.LocalDir()
fi, err := os.ReadDir(localdir)
if err != nil && len(args) == 0 {
log.Fatal(err)
diff --git a/counter/countertest/countertest.go b/counter/countertest/countertest.go
index c2f41f6..dc8bb11 100644
--- a/counter/countertest/countertest.go
+++ b/counter/countertest/countertest.go
@@ -7,7 +7,6 @@
package countertest
import (
- "path/filepath"
"sync"
"golang.org/x/telemetry/counter"
@@ -39,9 +38,7 @@
if opened {
panic("Open was called more than once")
}
- telemetry.ModeFile = telemetry.ModeFilePath(filepath.Join(telemetryDir, "mode"))
- telemetry.LocalDir = filepath.Join(telemetryDir, "local")
- telemetry.UploadDir = filepath.Join(telemetryDir, "upload")
+ telemetry.Default = telemetry.NewDir(telemetryDir)
counter.Open()
opened = true
diff --git a/internal/counter/counter_test.go b/internal/counter/counter_test.go
index ba5dfab..e3d6ff5 100644
--- a/internal/counter/counter_test.go
+++ b/internal/counter/counter_test.go
@@ -70,7 +70,7 @@
func TestMissingLocalDir(t *testing.T) {
testenv.SkipIfUnsupportedPlatform(t)
- err := os.RemoveAll(telemetry.LocalDir)
+ err := os.RemoveAll(telemetry.Default.LocalDir())
if err != nil {
t.Fatal(err)
}
@@ -236,7 +236,7 @@
var f file
c := f.New("gophers")
// shouldn't see a file yet
- fi, err := os.ReadDir(telemetry.LocalDir)
+ fi, err := os.ReadDir(telemetry.Default.LocalDir())
if err != nil {
t.Fatal(err)
}
@@ -245,7 +245,7 @@
}
c.Add(9)
// still shouldn't see a file
- fi, err = os.ReadDir(telemetry.LocalDir)
+ fi, err = os.ReadDir(telemetry.Default.LocalDir())
if err != nil {
close(&f)
t.Fatal(err)
@@ -256,7 +256,7 @@
}
f.rotate()
// now we should see a count file and a weekends file
- fi, _ = os.ReadDir(telemetry.LocalDir)
+ fi, _ = os.ReadDir(telemetry.Default.LocalDir())
if len(fi) != 2 {
close(&f)
t.Fatalf("len(fi) = %d, want 2", len(fi))
@@ -267,7 +267,7 @@
case "weekends":
weekendsFile = f.Name()
// while we're here, check that is ok
- buf, err := os.ReadFile(filepath.Join(telemetry.LocalDir, weekendsFile))
+ buf, err := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), weekendsFile))
if err != nil {
t.Fatal(err)
}
@@ -280,7 +280,7 @@
}
}
- buf, err := os.ReadFile(filepath.Join(telemetry.LocalDir, countFile))
+ buf, err := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), countFile))
if err != nil {
close(&f)
t.Fatal(err)
@@ -303,8 +303,8 @@
}
close(&f)
// remove the file for the next iteration of the loop
- os.Remove(filepath.Join(telemetry.LocalDir, countFile))
- os.Remove(filepath.Join(telemetry.LocalDir, weekendsFile))
+ os.Remove(filepath.Join(telemetry.Default.LocalDir(), countFile))
+ os.Remove(filepath.Join(telemetry.Default.LocalDir(), weekendsFile))
}
}
@@ -316,12 +316,12 @@
for i := 0; i < 7; i++ {
counterTime = future(i)
for index := range "0123456" {
- os.WriteFile(filepath.Join(telemetry.LocalDir, "weekends"), []byte{byte(index + '0')}, 0666)
+ os.WriteFile(filepath.Join(telemetry.Default.LocalDir(), "weekends"), []byte{byte(index + '0')}, 0666)
var f file
c := f.New("gophers")
c.Add(7)
f.rotate()
- fis, err := os.ReadDir(telemetry.LocalDir)
+ fis, err := os.ReadDir(telemetry.Default.LocalDir())
if err != nil {
t.Fatal(err)
}
@@ -330,11 +330,11 @@
for _, fi := range fis {
// ignore errors for brevity: something else will fail
if fi.Name() == "weekends" {
- buf, _ := os.ReadFile(filepath.Join(telemetry.LocalDir, fi.Name()))
+ buf, _ := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), fi.Name()))
buf = bytes.TrimSpace(buf)
weekends = time.Weekday(buf[0] - '0')
} else if strings.HasSuffix(fi.Name(), ".count") {
- buf, _ := os.ReadFile(filepath.Join(telemetry.LocalDir, fi.Name()))
+ buf, _ := os.ReadFile(filepath.Join(telemetry.Default.LocalDir(), fi.Name()))
parsed, _ := Parse(fi.Name(), buf)
begins, _ = time.Parse(time.RFC3339, parsed.Meta["TimeBegin"])
ends, _ = time.Parse(time.RFC3339, parsed.Meta["TimeEnd"])
@@ -361,7 +361,7 @@
close(&f)
// remove files for the next iteration of the loop
for _, f := range fis {
- os.Remove(filepath.Join(telemetry.LocalDir, f.Name()))
+ os.Remove(filepath.Join(telemetry.Default.LocalDir(), f.Name()))
}
}
}
@@ -505,13 +505,9 @@
func setup(t *testing.T) {
log.SetFlags(log.Lshortfile)
- tmpDir := t.TempDir() // new dir for each test
- telemetry.LocalDir = tmpDir + "/local"
- telemetry.UploadDir = tmpDir + "/upload"
- os.MkdirAll(telemetry.LocalDir, 0777)
- os.MkdirAll(telemetry.UploadDir, 0777)
- telemetry.ModeFile = telemetry.ModeFilePath(filepath.Join(tmpDir, "mode"))
- // os.UserConfigDir() is "" in tests so no point in looking at it
+ telemetry.Default = telemetry.NewDir(t.TempDir()) // new dir for each test
+ os.MkdirAll(telemetry.Default.LocalDir(), 0777)
+ os.MkdirAll(telemetry.Default.UploadDir(), 0777)
}
func restore() {
diff --git a/internal/counter/file.go b/internal/counter/file.go
index 742b1fc..12181b2 100644
--- a/internal/counter/file.go
+++ b/internal/counter/file.go
@@ -124,11 +124,11 @@
f.err = errNoBuildInfo
return
}
- if mode, _ := telemetry.Mode(); mode == "off" {
+ if mode, _ := telemetry.Default.Mode(); mode == "off" {
f.err = ErrDisabled
return
}
- dir := telemetry.LocalDir
+ dir := telemetry.Default.LocalDir()
if err := os.MkdirAll(dir, 0777); err != nil {
f.err = err
@@ -203,11 +203,11 @@
// If there is no 'weekends' file create it and initialize it
// to a random day of the week. There is a short interval for
// a race.
- weekends := filepath.Join(telemetry.LocalDir, "weekends")
+ weekends := filepath.Join(telemetry.Default.LocalDir(), "weekends")
day := fmt.Sprintf("%d\n", rand.Intn(7))
if _, err := os.ReadFile(weekends); err != nil {
- if err := os.MkdirAll(telemetry.LocalDir, 0777); err != nil {
- debugPrintf("%v: could not create telemetry.LocalDir %s", err, telemetry.LocalDir)
+ if err := os.MkdirAll(telemetry.Default.LocalDir(), 0777); err != nil {
+ debugPrintf("%v: could not create telemetry.LocalDir %s", err, telemetry.Default.LocalDir())
return 7, err
}
if err = os.WriteFile(weekends, []byte(day), 0666); err != nil {
@@ -357,7 +357,7 @@
if telemetry.DisabledOnPlatform {
return func() {}
}
- if mode, _ := telemetry.Mode(); mode == "off" {
+ if mode, _ := telemetry.Default.Mode(); mode == "off" {
// Don't open the file when telemetry is off.
defaultFile.err = ErrDisabled
return func() {} // No need to clean up.
diff --git a/internal/counter/rotate_test.go b/internal/counter/rotate_test.go
index 8b2c390..9e8ae49 100644
--- a/internal/counter/rotate_test.go
+++ b/internal/counter/rotate_test.go
@@ -147,7 +147,7 @@
setup(t)
defer restore()
// pretend something was uploaded
- os.WriteFile(filepath.Join(telemetry.UploadDir, "anything"), []byte{}, 0666)
+ os.WriteFile(filepath.Join(telemetry.Default.UploadDir(), "anything"), []byte{}, 0666)
var f file
defer close(&f)
c := f.New("gophers")
@@ -156,7 +156,7 @@
for i := 0; i < 2; i++ {
// nothing should change on the second rotate
f.rotate()
- fi, err := os.ReadDir(telemetry.LocalDir)
+ fi, err := os.ReadDir(telemetry.Default.LocalDir())
if err != nil || len(fi) != 2 {
t.Fatalf("err=%v, len(fi) = %d, want 2", err, len(fi))
}
@@ -170,7 +170,7 @@
if us != now {
t.Errorf("us = %v, want %v, i=%d y=%s", us, now, i, y)
}
- fd, err := os.Open(filepath.Join(telemetry.LocalDir, fi[0].Name()))
+ fd, err := os.Open(filepath.Join(telemetry.Default.LocalDir(), fi[0].Name()))
if err != nil {
t.Fatal(err)
}
@@ -189,7 +189,7 @@
}
counterTime = func() time.Time { return now.Add(7 * 24 * time.Hour) }
f.rotate()
- fi, err := os.ReadDir(telemetry.LocalDir)
+ fi, err := os.ReadDir(telemetry.Default.LocalDir())
if err != nil || len(fi) != 3 {
t.Fatalf("err=%v, len(fi) = %d, want 3", err, len(fi))
}
diff --git a/internal/regtest/e2e_test.go b/internal/regtest/e2e_test.go
index 9346c3d..d2ff7d3 100644
--- a/internal/regtest/e2e_test.go
+++ b/internal/regtest/e2e_test.go
@@ -73,20 +73,19 @@
for _, test := range tests {
t.Run(fmt.Sprintf("mode=%s", test.mode), func(t *testing.T) {
- telemetryDir := t.TempDir()
+ dir := telemetry.NewDir(t.TempDir())
if test.mode != "" {
- if err := telemetry.ModeFilePath(filepath.Join(telemetryDir, "mode")).SetMode(test.mode); err != nil {
+ if err := dir.SetMode(test.mode); err != nil {
t.Fatalf("SetMode failed: %v", err)
}
}
- out, err := RunProg(t, telemetryDir, prog)
+ out, err := RunProg(t, dir.Dir(), prog)
if err != nil {
t.Fatalf("program failed unexpectedly (%v)\n%s", err, out)
}
- localDir := filepath.Join(telemetryDir, "local")
- _, err = os.Stat(localDir)
+ _, err = os.Stat(dir.LocalDir())
if err != nil && !os.IsNotExist(err) {
- t.Fatalf("os.Stat(%q): unexpected error: %v", localDir, err)
+ t.Fatalf("os.Stat(%q): unexpected error: %v", dir.LocalDir(), err)
}
if gotLocalDir := err == nil; gotLocalDir != test.wantLocalDir {
t.Errorf("got /local dir: %v, want %v; out:\n%s", gotLocalDir, test.wantLocalDir, string(out))
diff --git a/internal/telemetry/dir.go b/internal/telemetry/dir.go
new file mode 100644
index 0000000..62f9a75
--- /dev/null
+++ b/internal/telemetry/dir.go
@@ -0,0 +1,155 @@
+// 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 telemetry manages the telemetry mode file.
+package telemetry
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "time"
+)
+
+// Default is the default directory containing Go telemetry configuration and
+// data.
+//
+// If Default is uninitialized, Default.Mode will be "off". As a consequence,
+// no data should be written to the directory, and so the path values of
+// LocalDir, UploadDir, etc. must not matter.
+//
+// Default is a global for convenience and testing, but should not be mutated
+// outside of tests.
+//
+// TODO(rfindley): it would be nice to completely eliminate this global state,
+// or at least push it in the golang.org/x/telemetry package
+var Default Dir
+
+// A Dir holds paths to telemetry data inside a directory.
+type Dir struct {
+ dir, local, upload, modefile string
+}
+
+// NewDir creates a new Dir encapsulating paths in the given dir.
+//
+// NewDir does not create any new directories or files--it merely encapsulates
+// the telemetry directory layout.
+func NewDir(dir string) Dir {
+ return Dir{
+ dir: dir,
+ local: filepath.Join(dir, "local"),
+ upload: filepath.Join(dir, "upload"),
+ modefile: filepath.Join(dir, "mode"),
+ }
+}
+
+func init() {
+ cfgDir, err := os.UserConfigDir()
+ if err != nil {
+ return
+ }
+ Default = NewDir(filepath.Join(cfgDir, "go", "telemetry"))
+}
+
+func (d Dir) Dir() string {
+ return d.dir
+}
+
+func (d Dir) LocalDir() string {
+ return d.local
+}
+
+func (d Dir) UploadDir() string {
+ return d.upload
+}
+
+func (d Dir) ModeFile() string {
+ return d.modefile
+}
+
+// SetMode updates the telemetry mode with the given mode.
+// Acceptable values for mode are "on", "off", or "local".
+//
+// SetMode always writes the mode file, and explicitly records the date at
+// which the modefile was updated. This means that calling SetMode with "on"
+// effectively resets the timeout before the next telemetry report is uploaded.
+func (d Dir) SetMode(mode string) error {
+ return d.SetModeAsOf(mode, time.Now())
+}
+
+// SetModeAsOf is like SetMode, but accepts an explicit time to use to
+// back-date the mode state. This exists only for testing purposes.
+func (d Dir) SetModeAsOf(mode string, asofTime time.Time) error {
+ mode = strings.TrimSpace(mode)
+ switch mode {
+ case "on", "off", "local":
+ default:
+ return fmt.Errorf("invalid telemetry mode: %q", mode)
+ }
+ if d.modefile == "" {
+ return fmt.Errorf("cannot determine telemetry mode file name")
+ }
+ // TODO(rfindley): why is this not 777, consistent with the use of 666 below?
+ if err := os.MkdirAll(filepath.Dir(d.modefile), 0755); err != nil {
+ return fmt.Errorf("cannot create a telemetry mode file: %w", err)
+ }
+
+ asof := asofTime.UTC().Format("2006-01-02")
+ // Defensively guarantee that we can parse the asof time.
+ if _, err := time.Parse("2006-01-02", asof); err != nil {
+ return fmt.Errorf("internal error: invalid mode date %q: %v", asof, err)
+ }
+
+ data := []byte(mode + " " + asof)
+ return os.WriteFile(d.modefile, data, 0666)
+}
+
+// Mode returns the current telemetry mode, as well as the time that the mode
+// was effective.
+//
+// If there is no effective time, the second result is the zero time.
+//
+// If Mode is "off", no data should be written to the telemetry directory, and
+// the other paths values referenced by Dir should be considered undefined.
+// This accounts for the case where initializing [Default] fails, and therefore
+// local telemetry paths are unknown.
+func (d Dir) Mode() (string, time.Time) {
+ if d.modefile == "" {
+ return "off", time.Time{} // it's likely LocalDir/UploadDir are empty too. Turn off telemetry.
+ }
+ data, err := os.ReadFile(d.modefile)
+ if err != nil {
+ return "local", time.Time{} // default
+ }
+ mode := string(data)
+ mode = strings.TrimSpace(mode)
+
+ // Forward compatibility for https://go.dev/issue/63142#issuecomment-1734025130
+ //
+ // If the modefile contains a date, return it.
+ if idx := strings.Index(mode, " "); idx >= 0 {
+ d, err := time.Parse("2006-01-02", mode[idx+1:])
+ if err != nil {
+ d = time.Time{}
+ }
+ return mode[:idx], d
+ }
+
+ return mode, time.Time{}
+}
+
+// DisabledOnPlatform indicates whether telemetry is disabled
+// due to bugs in the current platform.
+const DisabledOnPlatform = false ||
+ // The following platforms could potentially be supported in the future:
+ runtime.GOOS == "openbsd" || // #60614
+ runtime.GOOS == "solaris" || // #60968 #60970
+ runtime.GOOS == "android" || // #60967
+ runtime.GOOS == "illumos" || // #65544
+ // These platforms fundamentally can't be supported:
+ runtime.GOOS == "js" || // #60971
+ runtime.GOOS == "wasip1" || // #60971
+ runtime.GOOS == "plan9" // https://github.com/golang/go/issues/57540#issuecomment-1470766639
diff --git a/internal/telemetry/mode_test.go b/internal/telemetry/dir_test.go
similarity index 63%
rename from internal/telemetry/mode_test.go
rename to internal/telemetry/dir_test.go
index ebde64b..aafe06b 100644
--- a/internal/telemetry/mode_test.go
+++ b/internal/telemetry/dir_test.go
@@ -6,42 +6,38 @@
package telemetry
import (
- "fmt"
"os"
- "path/filepath"
"testing"
"time"
)
-func TestTelemetryDefault(t *testing.T) {
+func TestDefaults(t *testing.T) {
defaultDirMissing := false
if _, err := os.UserConfigDir(); err != nil {
defaultDirMissing = true
}
if defaultDirMissing {
- if LocalDir != "" || UploadDir != "" || ModeFile != "" {
- t.Errorf("DefaultSetting: (%q, %q, %q), want empty LocalDir/UploadDir/ModeFile", LocalDir, UploadDir, ModeFile)
+ if Default.LocalDir() != "" || Default.UploadDir() != "" || Default.ModeFile() != "" {
+ t.Errorf("DefaultSetting: (%q, %q, %q), want empty LocalDir/UploadDir/ModeFile", Default.LocalDir(), Default.UploadDir(), Default.ModeFile())
}
} else {
- if LocalDir == "" || UploadDir == "" || ModeFile == "" {
- t.Errorf("DefaultSetting: (%q, %q, %q), want non-empty LocalDir/UploadDir/ModeFile", LocalDir, UploadDir, ModeFile)
+ if Default.LocalDir() == "" || Default.UploadDir() == "" || Default.ModeFile() == "" {
+ t.Errorf("DefaultSetting: (%q, %q, %q), want non-empty LocalDir/UploadDir/ModeFile", Default.LocalDir(), Default.UploadDir(), Default.ModeFile())
}
}
}
func TestTelemetryModeWithNoModeConfig(t *testing.T) {
- tmp := t.TempDir()
tests := []struct {
- modefile ModeFilePath
- want string
+ dir Dir
+ want string
}{
- {ModeFilePath(filepath.Join(tmp, "mode")), "local"},
- {"", "off"},
+ {NewDir(t.TempDir()), "local"},
+ {Dir{}, "off"},
}
for _, tt := range tests {
- if got, _ := tt.modefile.Mode(); got != tt.want {
- t.Logf("Mode file: %q", tt.modefile)
- t.Errorf("Mode() = %v, want %v", got, tt.want)
+ if got, _ := tt.dir.Mode(); got != tt.want {
+ t.Errorf("Dir{modefile=%q}.Mode() = %v, want %v", tt.dir.ModeFile(), got, tt.want)
}
}
}
@@ -59,18 +55,17 @@
{"bogus", true},
{"", true},
}
- tmp := t.TempDir()
- for i, tt := range tests {
+ for _, tt := range tests {
t.Run("mode="+tt.in, func(t *testing.T) {
- modefile := ModeFilePath(filepath.Join(tmp, fmt.Sprintf("modefile%d", i)))
- setErr := modefile.SetMode(tt.in)
+ dir := NewDir(t.TempDir())
+ setErr := dir.SetMode(tt.in)
if (setErr != nil) != tt.wantErr {
t.Fatalf("Set() error = %v, wantErr %v", setErr, tt.wantErr)
}
if setErr != nil {
return
}
- if got, _ := modefile.Mode(); got != tt.in {
+ if got, _ := dir.Mode(); got != tt.in {
t.Errorf("LookupMode() = %q, want %q", got, tt.in)
}
})
@@ -88,16 +83,15 @@
{"off", "off", time.Time{}},
{"local", "local", time.Time{}},
}
- tmp := t.TempDir()
- for i, tt := range tests {
+ for _, tt := range tests {
t.Run("mode="+tt.in, func(t *testing.T) {
- fname := filepath.Join(tmp, fmt.Sprintf("modefile%d", i))
- if err := os.WriteFile(fname, []byte(tt.in), 0666); err != nil {
+ dir := NewDir(t.TempDir())
+ if err := os.WriteFile(dir.ModeFile(), []byte(tt.in), 0666); err != nil {
t.Fatal(err)
}
// Note: the checks below intentionally do not use time.Equal:
// we want this exact representation of time.
- if gotMode, gotTime := ModeFilePath(fname).Mode(); gotMode != tt.wantMode || gotTime != tt.wantTime {
+ if gotMode, gotTime := dir.Mode(); gotMode != tt.wantMode || gotTime != tt.wantTime {
t.Errorf("ModeFilePath(contents=%s).Mode() = %q, %v, want %q, %v", tt.in, gotMode, gotTime, tt.wantMode, tt.wantTime)
}
})
diff --git a/internal/telemetry/mode.go b/internal/telemetry/mode.go
deleted file mode 100644
index d7d3f24..0000000
--- a/internal/telemetry/mode.go
+++ /dev/null
@@ -1,131 +0,0 @@
-// 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 telemetry manages the telemetry mode file.
-package telemetry
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "runtime"
- "strings"
- "time"
-)
-
-// The followings are the process' default Settings.
-// The values are subdirectories and a file under
-// os.UserConfigDir()/go/telemetry.
-// For convenience, each field is made to global
-// and they are not supposed to be changed.
-var (
- // Default directory containing count files, local reports (not yet uploaded), and logs
- LocalDir string
- // Default directory containing uploaded reports.
- UploadDir string
- // Default file path that holds the telemetry mode info.
- ModeFile ModeFilePath
-)
-
-// ModeFilePath is the telemetry mode file path with methods to manipulate the file contents.
-type ModeFilePath string
-
-func init() {
- cfgDir, err := os.UserConfigDir()
- if err != nil {
- return
- }
- gotelemetrydir := filepath.Join(cfgDir, "go", "telemetry")
- LocalDir = filepath.Join(gotelemetrydir, "local")
- UploadDir = filepath.Join(gotelemetrydir, "upload")
- ModeFile = ModeFilePath(filepath.Join(gotelemetrydir, "mode"))
-}
-
-// SetMode updates the telemetry mode with the given mode.
-// Acceptable values for mode are "on", "off", or "local".
-//
-// SetMode always writes the mode file, and explicitly records the date at
-// which the modefile was updated. This means that calling SetMode with "on"
-// effectively resets the timeout before the next telemetry report is uploaded.
-func SetMode(mode string) error {
- return ModeFile.SetMode(mode)
-}
-
-func (m ModeFilePath) SetMode(mode string) error {
- return m.SetModeAsOf(mode, time.Now())
-}
-
-// SetModeAsOf is like SetMode, but accepts an explicit time to use to
-// back-date the mode state. This exists only for testing purposes.
-func (m ModeFilePath) SetModeAsOf(mode string, asofTime time.Time) error {
- mode = strings.TrimSpace(mode)
- switch mode {
- case "on", "off", "local":
- default:
- return fmt.Errorf("invalid telemetry mode: %q", mode)
- }
- fname := string(m)
- if fname == "" {
- return fmt.Errorf("cannot determine telemetry mode file name")
- }
- if err := os.MkdirAll(filepath.Dir(fname), 0755); err != nil {
- return fmt.Errorf("cannot create a telemetry mode file: %w", err)
- }
-
- asof := asofTime.UTC().Format("2006-01-02")
- // Defensively guarantee that we can parse the asof time.
- if _, err := time.Parse("2006-01-02", asof); err != nil {
- return fmt.Errorf("internal error: invalid mode date %q: %v", asof, err)
- }
-
- data := []byte(mode + " " + asof)
- return os.WriteFile(fname, data, 0666)
-}
-
-// Mode returns the current telemetry mode, as well as the time that the mode
-// was effective.
-//
-// If there is no effective time, the second result is the zero time.
-func Mode() (string, time.Time) {
- return ModeFile.Mode()
-}
-
-func (m ModeFilePath) Mode() (string, time.Time) {
- fname := string(m)
- if fname == "" {
- return "off", time.Time{} // it's likely LocalDir/UploadDir are empty too. Turn off telemetry.
- }
- data, err := os.ReadFile(fname)
- if err != nil {
- return "local", time.Time{} // default
- }
- mode := string(data)
- mode = strings.TrimSpace(mode)
-
- // Forward compatibility for https://go.dev/issue/63142#issuecomment-1734025130
- //
- // If the modefile contains a date, return it.
- if idx := strings.Index(mode, " "); idx >= 0 {
- d, err := time.Parse("2006-01-02", mode[idx+1:])
- if err != nil {
- d = time.Time{}
- }
- return mode[:idx], d
- }
-
- return mode, time.Time{}
-}
-
-// DisabledOnPlatform indicates whether telemetry is disabled
-// due to bugs in the current platform.
-const DisabledOnPlatform = false ||
- // The following platforms could potentially be supported in the future:
- runtime.GOOS == "openbsd" || // #60614
- runtime.GOOS == "solaris" || // #60968 #60970
- runtime.GOOS == "android" || // #60967
- runtime.GOOS == "illumos" || // #65544
- // These platforms fundamentally can't be supported:
- runtime.GOOS == "js" || // #60971
- runtime.GOOS == "wasip1" || // #60971
- runtime.GOOS == "plan9" // https://github.com/golang/go/issues/57540#issuecomment-1470766639
diff --git a/internal/upload/dates_test.go b/internal/upload/dates_test.go
index 495ac2e..bf41e37 100644
--- a/internal/upload/dates_test.go
+++ b/internal/upload/dates_test.go
@@ -66,24 +66,21 @@
// and scheduled to get expired in the future.
// Let's pretend telemetry was enabled a year ago by mutating the mode file,
// we are in the future, and test if the count files are successfully uploaded.
- uploader.ModeFilePath.SetModeAsOf("on", uploader.StartTime.Add(-365*24*time.Hour).UTC())
- uploadedContent, fname := subtest(t, uc, uploader) // TODO(hyangah) : inline
+ uploader.Dir.SetModeAsOf("on", uploader.StartTime.Add(-365*24*time.Hour).UTC())
+ uploadedContent, fname := subtest(t, uploader) // TODO(hyangah) : inline
if want, got := [][]byte{uploadedContent}, uploaded(); !reflect.DeepEqual(want, got) {
t.Errorf("server got %s\nwant %s", got, want)
}
// and check that the uploaded report is in the upload dir
- uname := filepath.Join(uploader.UploadDir, fname)
+ uname := filepath.Join(uploader.Dir.UploadDir(), fname)
if _, err := os.Stat(uname); err != nil {
t.Errorf("%v for uploade report %s", err, uname)
}
}
func newTestUploader(uc *telemetry.UploadConfig, telemetryDir string, srv *httptest.Server) *Uploader {
- uploader := NewUploader(uc)
- uploader.LocalDir = filepath.Join(telemetryDir, "local")
- uploader.UploadDir = filepath.Join(telemetryDir, "upload")
- uploader.ModeFilePath = telemetry.ModeFilePath(filepath.Join(telemetryDir, "mode"))
+ uploader := NewUploader(telemetryDir, uc)
uploader.UploadServerURL = srv.URL
return uploader
}
@@ -103,7 +100,7 @@
}
// createFailingUploadServer creates a test server that returns errors
-func createFailingUploadServer(t *testing.T) *httptest.Server {
+func createFailingUploadServer() *httptest.Server {
f := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.ReadAll(r.Body)
http.Error(w, "failed", http.StatusBadRequest)
@@ -130,7 +127,7 @@
uc := createTestUploadConfig(t, []string{"knownCounter"}, []string{"aStack"})
// Start upload server
- srv := createFailingUploadServer(t)
+ srv := createFailingUploadServer()
defer srv.Close()
uploader := newTestUploader(uc, telemetryDir, srv)
@@ -151,11 +148,11 @@
// and scheduled to get expired in the future.
// Let's pretend telemetry was enabled a year ago by mutating the mode file,
// we are in the future, and test if the count files are successfully uploaded.
- uploader.ModeFilePath.SetModeAsOf("on", uploader.StartTime.Add(-365*24*time.Hour).UTC())
- _, fname := subtest(t, uc, uploader)
+ uploader.Dir.SetModeAsOf("on", uploader.StartTime.Add(-365*24*time.Hour).UTC())
+ _, fname := subtest(t, uploader)
// check that fname does not exist
- lname := filepath.Join(uploader.LocalDir, fname)
- uname := filepath.Join(uploader.UploadDir, fname)
+ lname := filepath.Join(uploader.Dir.LocalDir(), fname)
+ uname := filepath.Join(uploader.Dir.UploadDir(), fname)
if _, err := os.Stat(lname); err == nil {
t.Errorf("local file %s should not exist", lname)
}
@@ -322,7 +319,7 @@
defer srv.Close()
uploader := newTestUploader(uc, telemetryDir, srv)
- uploader.ModeFilePath.SetModeAsOf("on", telemetryEnableTime)
+ uploader.Dir.SetModeAsOf("on", telemetryEnableTime)
uploader.UploadServerURL = srv.URL
uploader.StartTime = mustParseDate(tx.today)
@@ -441,13 +438,13 @@
}
func doTest(t *testing.T, u *Uploader, doing *Test, known *countFileInfo) int {
- dbg := filepath.Join(u.LocalDir, "debug")
+ dbg := filepath.Join(u.Dir.LocalDir(), "debug")
os.MkdirAll(dbg, 0777)
if err := LogIfDebug(dbg); err != nil {
t.Errorf("debug logging: %v", err)
}
if len(doing.uploads) > 0 {
- os.MkdirAll(u.UploadDir, 0777)
+ os.MkdirAll(u.Dir.UploadDir(), 0777)
}
contents := bytes.Join([][]byte{
known.buf[:known.beginOffset],
@@ -457,25 +454,25 @@
known.buf[known.endOffset+len("YYYY-MM-DD"):],
}, nil)
filename := known.namePrefix + doing.date + ".v1.count"
- if err := os.WriteFile(filepath.Join(u.LocalDir, filename), contents, 0666); err != nil {
+ if err := os.WriteFile(filepath.Join(u.Dir.LocalDir(), filename), contents, 0666); err != nil {
t.Errorf("%v writing count file for %s (%s)", err, doing.name, filename)
return 0
}
for _, x := range doing.locals {
nm := fmt.Sprintf("local.%s.json", x)
- if err := os.WriteFile(filepath.Join(u.LocalDir, nm), []byte{}, 0666); err != nil {
+ if err := os.WriteFile(filepath.Join(u.Dir.LocalDir(), nm), []byte{}, 0666); err != nil {
t.Errorf("%v writing local file %s", err, nm)
}
}
for _, x := range doing.readys {
nm := fmt.Sprintf("%s.json", x)
- if err := os.WriteFile(filepath.Join(u.LocalDir, nm), []byte{}, 0666); err != nil {
+ if err := os.WriteFile(filepath.Join(u.Dir.LocalDir(), nm), []byte{}, 0666); err != nil {
t.Errorf("%v writing ready file %s", err, nm)
}
}
for _, x := range doing.uploads {
nm := fmt.Sprintf("%s.json", x)
- if err := os.WriteFile(filepath.Join(u.UploadDir, nm), []byte{}, 0666); err != nil {
+ if err := os.WriteFile(filepath.Join(u.Dir.UploadDir(), nm), []byte{}, 0666); err != nil {
t.Errorf("%v writing upload %s", err, nm)
}
}
@@ -485,9 +482,9 @@
// check results
var cfiles, rfiles, lfiles, ufiles, logcnt int
- fis, err := os.ReadDir(u.LocalDir)
+ fis, err := os.ReadDir(u.Dir.LocalDir())
if err != nil {
- t.Errorf("%v reading localdir %s", err, u.LocalDir)
+ t.Errorf("%v reading localdir %s", err, u.Dir.LocalDir())
return 0
}
for _, f := range fis {
@@ -500,7 +497,7 @@
case strings.HasSuffix(f.Name(), ".json"):
rfiles++
case f.Name() == "debug":
- dbgname := filepath.Join(u.LocalDir, "debug")
+ dbgname := filepath.Join(u.Dir.LocalDir(), "debug")
logs, err := os.ReadDir(dbgname)
if err != nil {
break
@@ -513,9 +510,9 @@
if logcnt != 1 {
t.Errorf("expected 1 log file, got %d", logcnt)
}
- fis, err = os.ReadDir(u.UploadDir)
+ fis, err = os.ReadDir(u.Dir.UploadDir())
if err != nil {
- t.Errorf("%v reading uploaddir %s", err, u.UploadDir)
+ t.Errorf("%v reading uploaddir %s", err, u.Dir.UploadDir())
return 0
}
ufiles = len(fis) // assume there's nothing but .json reports
@@ -543,7 +540,7 @@
}
// check that generated report is as expected, and return its contents and its name
-func subtest(t *testing.T, c *telemetry.UploadConfig, u *Uploader) ([]byte, string) {
+func subtest(t *testing.T, u *Uploader) ([]byte, string) {
// check state before generating report
work := u.findWork()
// expect one count file and nothing else
@@ -569,7 +566,7 @@
// the uploadable report
t.Errorf("expected one readyfile, got %d", len(got.readyfiles))
}
- fi, err := os.ReadDir(u.LocalDir)
+ fi, err := os.ReadDir(u.Dir.LocalDir())
if len(fi) != 3 || err != nil {
// one local report, one uploadable report, one weekends file
t.Errorf("expected three files in LocalDir, got %d, %v", len(fi), err)
@@ -581,7 +578,7 @@
var localFile, uploadFile []byte
var uploadName string
for _, f := range fi {
- fname := filepath.Join(u.LocalDir, f.Name())
+ fname := filepath.Join(u.Dir.LocalDir(), f.Name())
buf, err := os.ReadFile(fname)
if err != nil {
t.Fatal(err)
diff --git a/internal/upload/findwork.go b/internal/upload/findwork.go
index 286d6be..da3a16a 100644
--- a/internal/upload/findwork.go
+++ b/internal/upload/findwork.go
@@ -23,7 +23,7 @@
// that need to be uploaded. (There may be unexpected leftover files
// and uploading is supposed to be idempotent.)
func (u *Uploader) findWork() work {
- localdir, uploaddir := u.LocalDir, u.UploadDir
+ localdir, uploaddir := u.Dir.LocalDir(), u.Dir.UploadDir()
var ans work
fis, err := os.ReadDir(localdir)
if err != nil {
@@ -31,7 +31,7 @@
return ans
}
- mode, asof := u.ModeFilePath.Mode()
+ mode, asof := u.Dir.Mode()
logger.Printf("mode %s, asof %s", mode, asof)
// count files end in .v1.count
diff --git a/internal/upload/reports.go b/internal/upload/reports.go
index e8a65bc..c679612 100644
--- a/internal/upload/reports.go
+++ b/internal/upload/reports.go
@@ -23,7 +23,7 @@
// reports generates reports from inactive count files
func (u *Uploader) reports(todo *work) ([]string, error) {
- if mode, _ := u.ModeFilePath.Mode(); mode == "off" {
+ if mode, _ := u.Dir.Mode(); mode == "off" {
return nil, nil // no reports
}
thisInstant := u.StartTime
@@ -120,7 +120,7 @@
u.ConfigVersion = v
}
uploadOK := true
- mode, asof := u.ModeFilePath.Mode()
+ mode, asof := u.Dir.Mode()
if u.Config == nil || mode != "on" {
logger.Printf("no upload config or mode %q is not 'on'", mode)
uploadOK = false // no config, nothing to upload
@@ -227,8 +227,8 @@
return "", fmt.Errorf("failed to marshal upload report (%v)", err)
}
}
- localFileName := filepath.Join(u.LocalDir, "local."+expiryDate+".json")
- uploadFileName := filepath.Join(u.LocalDir, expiryDate+".json")
+ localFileName := filepath.Join(u.Dir.LocalDir(), "local."+expiryDate+".json")
+ uploadFileName := filepath.Join(u.Dir.LocalDir(), expiryDate+".json")
/* Prepare to write files */
// if either file exists, someone has been here ahead of us
diff --git a/internal/upload/run.go b/internal/upload/run.go
index f21d973..beab886 100644
--- a/internal/upload/run.go
+++ b/internal/upload/run.go
@@ -44,7 +44,7 @@
// dirname, if it exists. If dirname is the empty string,
// the function tries the directory it.Localdir/debug.
func LogIfDebug(dirname string) error {
- dname := filepath.Join(telemetry.LocalDir, "debug")
+ dname := filepath.Join(telemetry.Default.LocalDir(), "debug")
if dirname != "" {
dname = dirname
}
@@ -93,12 +93,8 @@
// ConfigVersion is the version of the config.
ConfigVersion string
- // LocalDir is where the local counter files are.
- LocalDir string
- // UploadDir is where uploader leaves the copy of uploaded data.
- UploadDir string
- // ModeFilePath is the file.
- ModeFilePath telemetry.ModeFilePath
+ // Dir is the telemetry directory to process.
+ Dir telemetry.Dir
UploadServerURL string
StartTime time.Time
@@ -107,13 +103,24 @@
}
// NewUploader creates a default uploader.
-func NewUploader(config *telemetry.UploadConfig) *Uploader {
+//
+// If telemetryDir is set, it is used as the telemetry data directory to
+// process, instead of the default directory.
+//
+// If config is set, it is used as the upload config, rather than the
+// downloaded upload configuration.
+func NewUploader(telemetryDir string, config *telemetry.UploadConfig) *Uploader {
+ var dir telemetry.Dir
+ if telemetryDir != "" {
+ dir = telemetry.NewDir(telemetryDir)
+ } else {
+ dir = telemetry.Default
+ }
+ // TODO(rfindley): get the config here, rather than during the upload process.
return &Uploader{
Config: config,
ConfigVersion: "custom",
- LocalDir: telemetry.LocalDir,
- UploadDir: telemetry.UploadDir,
- ModeFilePath: telemetry.ModeFile,
+ Dir: dir,
UploadServerURL: "https://telemetry.go.dev/upload",
StartTime: time.Now().UTC(),
}
diff --git a/internal/upload/upload.go b/internal/upload/upload.go
index 9be10a7..c01f9c4 100644
--- a/internal/upload/upload.go
+++ b/internal/upload/upload.go
@@ -85,7 +85,7 @@
return false
}
// put a copy in the uploaded directory
- newname := filepath.Join(u.UploadDir, fdate+".json")
+ newname := filepath.Join(u.Dir.UploadDir(), fdate+".json")
if err := os.WriteFile(newname, buf, 0644); err == nil {
os.Remove(fname) // if it exists
}
diff --git a/mode.go b/mode.go
index fb9672c..1361d64 100644
--- a/mode.go
+++ b/mode.go
@@ -24,7 +24,7 @@
//
// [gotelemetry]: https://pkg.go.dev/golang.org/x/telemetry/cmd/gotelemetry
func Mode() string {
- mode, _ := telemetry.Mode()
+ mode, _ := telemetry.Default.Mode()
return mode
}
@@ -36,5 +36,5 @@
// An error is returned if the provided mode value is invalid, or if an error
// occurs while persisting the mode value to the file system.
func SetMode(mode string) error {
- return telemetry.SetMode(mode)
+ return telemetry.Default.SetMode(mode)
}
diff --git a/start.go b/start.go
index 6c88992..3cc5c1f 100644
--- a/start.go
+++ b/start.go
@@ -70,11 +70,9 @@
// be executed twice (parent and child).
func Start(config Config) {
if config.TelemetryDir != "" {
- telemetry.ModeFile = telemetry.ModeFilePath(filepath.Join(config.TelemetryDir, "mode"))
- telemetry.LocalDir = filepath.Join(config.TelemetryDir, "local")
- telemetry.UploadDir = filepath.Join(config.TelemetryDir, "upload")
+ telemetry.Default = telemetry.NewDir(config.TelemetryDir)
}
- mode, _ := telemetry.Mode()
+ mode, _ := telemetry.Default.Mode()
if mode == "off" {
// Telemetry is turned off. Crash reporting doesn't work without telemetry
// at least set to "local", and the uploader isn't started in uploaderChild if
@@ -84,7 +82,7 @@
counter.Open()
- if _, err := os.Stat(telemetry.LocalDir); err != nil {
+ if _, err := os.Stat(telemetry.Default.LocalDir()); err != nil {
// There was a problem statting LocalDir, which is needed for both
// crash monitoring and counter uploading. Most likely, there was an
// error creating telemetry.LocalDir in the counter.Open call above.
@@ -121,7 +119,7 @@
cmd := exec.Command(exe, "** telemetry **") // this unused arg is just for ps(1)
daemonize(cmd)
cmd.Env = append(os.Environ(), telemetryChildVar+"=1")
- cmd.Dir = telemetry.LocalDir
+ cmd.Dir = telemetry.Default.LocalDir()
// The child process must write to a log file, not
// the stderr file it inherited from the parent, as
@@ -132,7 +130,7 @@
// By default, we discard the child process's stderr,
// but in line with the uploader, log to a file in local/debug
// only if that directory was created by the user.
- localDebug := filepath.Join(telemetry.LocalDir, "debug")
+ localDebug := filepath.Join(telemetry.Default.LocalDir(), "debug")
fd, err := os.Stat(localDebug)
if err != nil {
if !os.IsNotExist(err) {
@@ -190,17 +188,17 @@
}
func uploaderChild() {
- if mode, _ := telemetry.Mode(); mode == "off" {
+ if mode, _ := telemetry.Default.Mode(); mode == "off" {
// There's no work to be done if telemetry is turned off.
return
}
- if telemetry.LocalDir == "" {
+ if telemetry.Default.LocalDir() == "" {
// The telemetry dir wasn't initialized properly, probably because
// os.UserConfigDir did not complete successfully. In that case
// there are no counters to upload, so we should just do nothing.
return
}
- tokenfilepath := filepath.Join(telemetry.LocalDir, "upload.token")
+ tokenfilepath := filepath.Join(telemetry.Default.LocalDir(), "upload.token")
ok, err := acquireUploadToken(tokenfilepath)
if err != nil {
log.Printf("error acquiring upload token: %v", err)
diff --git a/upload/upload.go b/upload/upload.go
index 122b725..bb2eecc 100644
--- a/upload/upload.go
+++ b/upload/upload.go
@@ -25,7 +25,7 @@
log.Printf("upload recover: %v", err)
}
}()
- upload.NewUploader(nil).Run()
+ upload.NewUploader("", nil).Run()
}
// A Control allows the user to override various default