blob: eba13b1a573560d3a4316df4512c735626c7b66c [file] [log] [blame]
// 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 upload
import (
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"runtime/debug"
"strings"
"time"
"golang.org/x/telemetry/internal/configstore"
"golang.org/x/telemetry/internal/telemetry"
)
// RunConfig configures non-default behavior of a call to Run.
//
// All fields are optional, for testing or observability.
type RunConfig struct {
TelemetryDir string // if set, overrides the telemetry data directory
UploadURL string // if set, overrides the telemetry upload endpoint
LogWriter io.Writer // if set, used for detailed logging of the upload process
Env []string // if set, appended to the config download environment
StartTime time.Time // if set, overrides the upload start time
}
// Run generates and uploads reports, as allowed by the mode file.
func Run(config RunConfig) error {
defer func() {
if err := recover(); err != nil {
log.Printf("upload recover: %v", err)
}
}()
uploader, err := newUploader(config)
if err != nil {
return err
}
defer uploader.Close()
return uploader.Run()
}
// uploader encapsulates a single upload operation, carrying parameters and
// shared state.
type uploader struct {
// config is used to select counters to upload.
config *telemetry.UploadConfig //
configVersion string // version of the config
dir telemetry.Dir // the telemetry dir to process
uploadServerURL string
startTime time.Time
cache parsedCache
logFile *os.File
logger *log.Logger
}
// newUploader creates a new uploader to use for running the upload for the
// given config.
//
// Uploaders should only be used for one call to [uploader.Run].
func newUploader(rcfg RunConfig) (*uploader, error) {
// Determine the upload directory.
var dir telemetry.Dir
if rcfg.TelemetryDir != "" {
dir = telemetry.NewDir(rcfg.TelemetryDir)
} else {
dir = telemetry.Default
}
// Determine the upload URL.
uploadURL := rcfg.UploadURL
if uploadURL == "" {
uploadURL = "https://telemetry.go.dev/upload"
}
// Determine the upload logger.
//
// This depends on the provided rcfg.LogWriter and the presence of
// dir.DebugDir, as follows:
// 1. If LogWriter is present, log to it.
// 2. If DebugDir is present, log to a file within it.
// 3. If both LogWriter and DebugDir are present, log to a multi writer.
// 4. If neither LogWriter nor DebugDir are present, log to a noop logger.
var logWriters []io.Writer
logFile, err := debugLogFile(dir.DebugDir())
if err != nil {
logFile = nil
}
if logFile != nil {
logWriters = append(logWriters, logFile)
}
if rcfg.LogWriter != nil {
logWriters = append(logWriters, rcfg.LogWriter)
}
var logWriter io.Writer
switch len(logWriters) {
case 0:
logWriter = io.Discard
case 1:
logWriter = logWriters[0]
default:
logWriter = io.MultiWriter(logWriters...)
}
logger := log.New(logWriter, "", log.Ltime|log.Lmicroseconds|log.Lshortfile)
// Fetch the upload config, if it is not provided.
config, configVersion, err := configstore.Download("latest", rcfg.Env)
if err != nil {
return nil, err
}
// Set the start time, if it is not provided.
startTime := time.Now().UTC()
if !rcfg.StartTime.IsZero() {
startTime = rcfg.StartTime
}
return &uploader{
config: config,
configVersion: configVersion,
dir: dir,
uploadServerURL: uploadURL,
startTime: startTime,
logFile: logFile,
logger: logger,
}, nil
}
// Close cleans up any resources associated with the uploader.
func (u *uploader) Close() error {
if u.logFile == nil {
return nil
}
return u.logFile.Close()
}
// Run generates and uploads reports
func (u *uploader) Run() error {
if telemetry.DisabledOnPlatform {
return nil
}
todo := u.findWork()
ready, err := u.reports(&todo)
if err != nil {
u.logger.Printf("Error building reports: %v", err)
return fmt.Errorf("reports failed: %v", err)
}
u.logger.Printf("Uploading %d reports", len(ready))
for _, f := range ready {
u.uploadReport(f)
}
return nil
}
// debugLogFile arranges to write a log file in the given debug directory, if
// it exists.
func debugLogFile(debugDir string) (*os.File, error) {
fd, err := os.Stat(debugDir)
if os.IsNotExist(err) {
return nil, nil
}
if err != nil {
return nil, err
}
if !fd.IsDir() {
return nil, fmt.Errorf("debug path %q is not a directory", debugDir)
}
info, ok := debug.ReadBuildInfo()
if !ok {
return nil, fmt.Errorf("no build info")
}
year, month, day := time.Now().UTC().Date()
goVers := info.GoVersion
// E.g., goVers:"go1.22-20240109-RC01 cl/597041403 +dcbe772469 X:loopvar"
words := strings.Fields(goVers)
goVers = words[0]
progPkgPath := info.Path
if progPkgPath == "" {
progPkgPath = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
}
prog := path.Base(progPkgPath)
progVers := info.Main.Version
if progVers == "(devel)" { // avoid special characters in created file names
progVers = "devel"
}
logBase := strings.ReplaceAll(
fmt.Sprintf("%s-%s-%s-%4d%02d%02d-%d.log", prog, progVers, goVers, year, month, day, os.Getpid()),
" ", "")
fname := filepath.Join(debugDir, logBase)
if _, err := os.Stat(fname); err == nil {
// This process previously called upload.Run
return nil, nil
}
f, err := os.OpenFile(fname, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
if os.IsExist(err) {
return nil, nil // this process previously called upload.Run
}
return nil, err
}
return f, nil
}