| // Copyright 2024 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package telemetry | |
| import ( | |
| "fmt" | |
| "log" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "sync" | |
| "time" | |
| "golang.org/x/sync/errgroup" | |
| "golang.org/x/telemetry/counter" | |
| "golang.org/x/telemetry/internal/crashmonitor" | |
| "golang.org/x/telemetry/internal/telemetry" | |
| "golang.org/x/telemetry/internal/upload" | |
| ) | |
| // Config controls the behavior of [Start]. | |
| type Config struct { | |
| // ReportCrashes, if set, will enable crash reporting. | |
| // ReportCrashes uses the [debug.SetCrashOutput] mechanism, which is a | |
| // process-wide resource. | |
| // Do not make other calls to that function within your application. | |
| // ReportCrashes is a non-functional unless the program is built with go1.23+. | |
| ReportCrashes bool | |
| // Upload causes this program to periodically upload approved counters | |
| // from the local telemetry database to telemetry.go.dev. | |
| // | |
| // This option has no effect unless the user has given consent | |
| // to enable data collection, for example by running | |
| // cmd/gotelemetry or affirming the gopls dialog. | |
| // | |
| // (This feature is expected to be used only by gopls. | |
| // Longer term, the go command may become the sole program | |
| // responsible for uploading.) | |
| Upload bool | |
| // TelemetryDir, if set, will specify an alternate telemetry | |
| // directory to write data to. If not set, it uses the default | |
| // directory. | |
| // This field is intended to be used for isolating testing environments. | |
| TelemetryDir string | |
| // UploadStartTime, if set, overrides the time used as the upload start time, | |
| // which is the time used by the upload logic to determine whether counter | |
| // file data should be uploaded. Only counter files that have expired before | |
| // the start time are considered for upload. | |
| // | |
| // This field can be used to simulate a future upload that collects recently | |
| // modified counters. | |
| UploadStartTime time.Time | |
| // UploadURL, if set, overrides the URL used to receive uploaded reports. If | |
| // unset, this URL defaults to https://telemetry.go.dev/upload. | |
| UploadURL string | |
| } | |
| // Start initializes telemetry using the specified configuration. | |
| // | |
| // Start opens the local telemetry database so that counter increment | |
| // operations are durably recorded in the local file system. | |
| // | |
| // If [Config.Upload] is set, and the user has opted in to telemetry | |
| // uploading, this process may attempt to upload approved counters | |
| // to telemetry.go.dev. | |
| // | |
| // If [Config.ReportCrashes] is set, any fatal crash will be | |
| // recorded by incrementing a counter named for the stack of the | |
| // first running goroutine in the traceback. | |
| // | |
| // If either of these flags is set, Start re-executes the current | |
| // executable as a child process, in a special mode in which it | |
| // acts as a telemetry sidecar for the parent process (the application). | |
| // In that mode, the call to Start will never return, so Start must | |
| // be called immediately within main, even before such things as | |
| // inspecting the command line. The application should avoid expensive | |
| // steps or external side effects in init functions, as they will | |
| // be executed twice (parent and child). | |
| // | |
| // Start returns a StartResult, which may be awaited via [StartResult.Wait] to | |
| // wait for all work done by Start to complete. | |
| func Start(config Config) *StartResult { | |
| switch v := os.Getenv(telemetryChildVar); v { | |
| case "": | |
| // The subprocess started by parent has GO_TELEMETRY_CHILD=1. | |
| return parent(config) | |
| case "1": | |
| child(config) // child will exit the process when it's done. | |
| case "2": | |
| // Do nothing: this was executed directly or indirectly by a child. | |
| default: | |
| log.Fatalf("unexpected value for %q: %q", telemetryChildVar, v) | |
| } | |
| return &StartResult{} | |
| } | |
| // MaybeChild executes the telemetry child logic if the calling program is | |
| // the telemetry child process, and does nothing otherwise. It is meant to be | |
| // called as the first thing in a program that uses telemetry.Start but cannot | |
| // call telemetry.Start immediately when it starts. | |
| func MaybeChild(config Config) { | |
| if v := os.Getenv(telemetryChildVar); v == "1" { | |
| child(config) // child will exit the process when it's done. | |
| } | |
| // other values of the telemetryChildVar environment variable | |
| // will be handled by telemetry.Start. | |
| } | |
| // A StartResult is a handle to the result of a call to [Start]. Call | |
| // [StartResult.Wait] to wait for the completion of all work done on behalf of | |
| // Start. | |
| type StartResult struct { | |
| wg sync.WaitGroup | |
| } | |
| // Wait waits for the completion of all work initiated by [Start]. | |
| func (res *StartResult) Wait() { | |
| if res == nil { | |
| return | |
| } | |
| res.wg.Wait() | |
| } | |
| var daemonize = func(cmd *exec.Cmd) {} | |
| // If telemetryChildVar is set to "1" in the environment, this is the telemetry | |
| // child. | |
| // | |
| // If telemetryChildVar is set to "2", this is a child of the child, and no | |
| // further forking should occur. | |
| const telemetryChildVar = "GO_TELEMETRY_CHILD" | |
| // If telemetryUploadVar is set to "1" in the environment, the upload token has been | |
| // acquired by the parent, and the child should attempt an upload. | |
| const telemetryUploadVar = "GO_TELEMETRY_CHILD_UPLOAD" | |
| func parent(config Config) *StartResult { | |
| if config.TelemetryDir != "" { | |
| telemetry.Default = telemetry.NewDir(config.TelemetryDir) | |
| } | |
| result := new(StartResult) | |
| mode, _ := telemetry.Default.Mode() | |
| if mode == "off" { | |
| // Telemetry is turned off. Crash reporting doesn't work without telemetry | |
| // at least set to "local". The upload process runs in both "on" and "local" modes. | |
| // In local mode the upload process builds local reports but does not do the upload. | |
| return result | |
| } | |
| counter.Open() | |
| 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. | |
| // Don't start the child. | |
| return result | |
| } | |
| childShouldUpload := config.Upload && acquireUploadToken() | |
| reportCrashes := config.ReportCrashes | |
| if reportCrashes || childShouldUpload { | |
| startChild(reportCrashes, childShouldUpload, result) | |
| } | |
| return result | |
| } | |
| func startChild(reportCrashes, upload bool, result *StartResult) { | |
| // This process is the application (parent). | |
| // Fork+exec the telemetry child. | |
| exe, err := os.Executable() | |
| if err != nil { | |
| // There was an error getting os.Executable. It's possible | |
| // for this to happen on AIX if os.Args[0] is not an absolute | |
| // path and we can't find os.Args[0] in PATH. | |
| log.Printf("failed to start telemetry sidecar: os.Executable: %v", err) | |
| return | |
| } | |
| cmd := exec.Command(exe, "** telemetry **") // this unused arg is just for ps(1) | |
| daemonize(cmd) | |
| cmd.Env = append(os.Environ(), telemetryChildVar+"=1") | |
| if upload { | |
| cmd.Env = append(cmd.Env, telemetryUploadVar+"=1") | |
| } | |
| cmd.Dir = telemetry.Default.LocalDir() | |
| // The child process must write to a log file, not | |
| // the stderr file it inherited from the parent, as | |
| // the child may outlive the parent but should not prolong | |
| // the life of any pipes created (by the grandparent) | |
| // to gather the output of the parent. | |
| // | |
| // By default, we discard the child process's stderr, | |
| // but in line with the uploader, log to a file in debug | |
| // only if that directory was created by the user. | |
| fd, err := os.Stat(telemetry.Default.DebugDir()) | |
| if err != nil { | |
| if !os.IsNotExist(err) { | |
| log.Printf("failed to stat debug directory: %v", err) | |
| return | |
| } | |
| } else if fd.IsDir() { | |
| // local/debug exists and is a directory. Set stderr to a log file path | |
| // in local/debug. | |
| childLogPath := filepath.Join(telemetry.Default.DebugDir(), "sidecar.log") | |
| childLog, err := os.OpenFile(childLogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) | |
| if err != nil { | |
| log.Printf("opening sidecar log file for child: %v", err) | |
| return | |
| } | |
| defer childLog.Close() | |
| cmd.Stderr = childLog | |
| } | |
| var crashOutputFile *os.File | |
| if reportCrashes { | |
| pipe, err := cmd.StdinPipe() | |
| if err != nil { | |
| log.Printf("StdinPipe: %v", err) | |
| return | |
| } | |
| crashOutputFile = pipe.(*os.File) // (this conversion is safe) | |
| } | |
| if err := cmd.Start(); err != nil { | |
| // The child couldn't be started. Log the failure. | |
| log.Printf("can't start telemetry child process: %v", err) | |
| return | |
| } | |
| if reportCrashes { | |
| crashmonitor.Parent(crashOutputFile) | |
| } | |
| result.wg.Add(1) | |
| go func() { | |
| cmd.Wait() // Release resources if cmd happens not to outlive this process. | |
| result.wg.Done() | |
| }() | |
| } | |
| func child(config Config) { | |
| log.SetPrefix(fmt.Sprintf("telemetry-sidecar (pid %v): ", os.Getpid())) | |
| if config.TelemetryDir != "" { | |
| telemetry.Default = telemetry.NewDir(config.TelemetryDir) | |
| } | |
| // golang/go#67211: be sure to set telemetryChildVar before running the | |
| // child, because the child itself invokes the go command to download the | |
| // upload config. If the telemetryChildVar variable is still set to "1", | |
| // that delegated go command may think that it is itself a telemetry | |
| // child. | |
| // | |
| // On the other hand, if telemetryChildVar were simply unset, then the | |
| // delegated go commands would fork themselves recursively. Short-circuit | |
| // this recursion. | |
| os.Setenv(telemetryChildVar, "2") | |
| upload := os.Getenv(telemetryUploadVar) == "1" | |
| // The crashmonitor and/or upload process may themselves record counters. | |
| counter.Open() | |
| // Start crashmonitoring and uploading depending on what's requested | |
| // and wait for the longer running child to complete before exiting: | |
| // if we collected a crash before the upload finished, wait for the | |
| // upload to finish before exiting | |
| var g errgroup.Group | |
| if config.ReportCrashes { | |
| g.Go(func() error { | |
| crashmonitor.Child() | |
| return nil | |
| }) | |
| } | |
| if upload { | |
| g.Go(func() error { | |
| uploaderChild(config.UploadStartTime, config.UploadURL) | |
| return nil | |
| }) | |
| } | |
| g.Wait() | |
| os.Exit(0) | |
| } | |
| func uploaderChild(asof time.Time, uploadURL string) { | |
| if err := upload.Run(upload.RunConfig{ | |
| UploadURL: uploadURL, | |
| LogWriter: os.Stderr, | |
| StartTime: asof, | |
| }); err != nil { | |
| log.Printf("upload failed: %v", err) | |
| } | |
| } | |
| // acquireUploadToken acquires a token permitting the caller to upload. | |
| // To limit the frequency of uploads, only one token is issue per | |
| // machine per time period. | |
| // The boolean indicates whether the token was acquired. | |
| func acquireUploadToken() bool { | |
| 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 false | |
| } | |
| tokenfile := filepath.Join(telemetry.Default.LocalDir(), "upload.token") | |
| const period = 24 * time.Hour | |
| // A process acquires a token by successfully creating a | |
| // well-known file. If the file already exists and has an | |
| // mtime age less then than the period, the process does | |
| // not acquire the token. If the file is older than the | |
| // period, the process is allowed to remove the file and | |
| // try to re-create it. | |
| fi, err := os.Stat(tokenfile) | |
| if err == nil { | |
| if time.Since(fi.ModTime()) < period { | |
| return false | |
| } | |
| // There's a possible race here where two processes check the | |
| // token file and see that it's older than the period, then the | |
| // first one removes it and creates another, and then a second one | |
| // removes the newly created file and creates yet another | |
| // file. Then both processes would act as though they had the token. | |
| // This is very rare, but it's also okay because we're only grabbing | |
| // the token to do rate limiting, not for correctness. | |
| _ = os.Remove(tokenfile) | |
| } else if !os.IsNotExist(err) { | |
| log.Printf("error acquiring upload taken: statting token file: %v", err) | |
| return false | |
| } | |
| f, err := os.OpenFile(tokenfile, os.O_CREATE|os.O_EXCL, 0666) | |
| if err != nil { | |
| if os.IsExist(err) { | |
| return false | |
| } | |
| log.Printf("error acquiring upload token: creating token file: %v", err) | |
| return false | |
| } | |
| _ = f.Close() | |
| return true | |
| } |