blob: 01c0dfda21d3ac6c1f0fe72eaa86b1d2598e87a0 [file] [log] [blame]
// 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})
}