| // 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 ( |
| "io" |
| "log" |
| "os" |
| "os/exec" |
| |
| "golang.org/x/sync/errgroup" |
| "golang.org/x/telemetry/counter" |
| "golang.org/x/telemetry/internal/crashmonitor" |
| "golang.org/x/telemetry/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 |
| |
| // Log messages generated by crash monitoring or uploading will be |
| // written to this optional writer. |
| Logger io.Writer |
| } |
| |
| // 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). |
| func Start(config Config) { |
| counter.Open() |
| |
| if (config.ReportCrashes && crashmonitor.Supported()) || config.Upload { |
| if os.Getenv(telemetryChildVar) != "" { |
| child(config) |
| panic("unreachable") |
| } |
| |
| parent(config) |
| } |
| } |
| |
| const telemetryChildVar = "X_TELEMETRY_CHILD" |
| |
| func parent(config Config) { |
| logger := log.New(config.Logger, "", 0) |
| // This process is the application (parent). |
| // Fork+exec the telemetry child. |
| exe, err := os.Executable() |
| if err != nil { |
| logger.Fatal(err) |
| } |
| cmd := exec.Command(exe, "** telemetry **") // this unused arg is just for ps(1) |
| cmd.Env = append(os.Environ(), telemetryChildVar+"=1") |
| cmd.Stderr = os.Stderr |
| cmd.Stdout = os.Stderr |
| |
| if config.ReportCrashes { |
| pipe, err := cmd.StdinPipe() |
| if err != nil { |
| logger.Fatalf("StdinPipe: %v", err) |
| } |
| |
| crashmonitor.Parent(pipe.(*os.File)) // (this conversion is safe) |
| } |
| |
| if err := cmd.Start(); err != nil { |
| logger.Fatalf("can't start telemetry child process: %v", err) |
| } |
| } |
| |
| func child(config Config) { |
| // 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.Upload { |
| g.Go(func() error { |
| uploaderChild(config.Logger) |
| return nil |
| }) |
| } |
| if config.ReportCrashes { |
| g.Go(func() error { |
| crashmonitor.Child(config.Logger) |
| return nil |
| }) |
| } |
| g.Wait() |
| } |
| |
| func uploaderChild(logger io.Writer) { |
| // TODO(matloob): Do rate-limiting here. |
| upload.Run(&upload.Control{Logger: logger}) |
| } |