227 lines
6.0 KiB
Go
227 lines
6.0 KiB
Go
![]() |
// 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.
|
||
|
var (
|
||
|
config *telemetry.UploadConfig
|
||
|
configVersion string
|
||
|
)
|
||
|
|
||
|
if mode, _ := dir.Mode(); mode == "on" {
|
||
|
// golang/go#68946: only download the upload config if it will be used.
|
||
|
//
|
||
|
// TODO(rfindley): This is a narrow change aimed at minimally fixing the
|
||
|
// associated bug. In the future, we should read the mode only once during
|
||
|
// the upload process.
|
||
|
config, configVersion, err = configstore.Download("latest", rcfg.Env)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
} else {
|
||
|
config = &telemetry.UploadConfig{}
|
||
|
configVersion = "v0.0.0-0"
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|