mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-07-22 11:18:04 +08:00
metrics: add build command duration metric
This adds a build duration metric for the build command with attributes related to the buildx driver, the error type (if any), and which options were used to perform the build from a subset of the options. This also refactors some of the utility methods used by the git tool to determine filepaths into its own separate package so they can be reused in another place. Also adds a test to ensure the resource is initialized correctly and doesn't error. The otel handler logging message is suppressed on buildx invocations so we never see the error if there's a problem with the schema url. It's so easy to mess up the schema url when upgrading OTEL that we need a proper test to make sure we haven't broken the functionality. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
34
util/confutil/node.go
Normal file
34
util/confutil/node.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package confutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var nodeIdentifierMu sync.Mutex
|
||||
|
||||
func TryNodeIdentifier(configDir string) (out string) {
|
||||
nodeIdentifierMu.Lock()
|
||||
defer nodeIdentifierMu.Unlock()
|
||||
sessionFile := filepath.Join(configDir, ".buildNodeID")
|
||||
if _, err := os.Lstat(sessionFile); err != nil {
|
||||
if os.IsNotExist(err) { // create a new file with stored randomness
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return out
|
||||
}
|
||||
if err := os.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dt, err := os.ReadFile(sessionFile)
|
||||
if err == nil {
|
||||
return string(dt)
|
||||
}
|
||||
return
|
||||
}
|
@@ -9,6 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -70,7 +71,7 @@ func (c *Git) RootDir() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return SanitizePath(root), nil
|
||||
return osutil.SanitizePath(root), nil
|
||||
}
|
||||
|
||||
func (c *Git) GitDir() (string, error) {
|
||||
|
@@ -7,8 +7,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/sys/mountinfo"
|
||||
)
|
||||
@@ -42,18 +40,3 @@ func gitPath(wd string) (string, error) {
|
||||
}
|
||||
return exec.LookPath("git")
|
||||
}
|
||||
|
||||
var windowsPathRegex = regexp.MustCompile(`^[A-Za-z]:[\\/].*$`)
|
||||
|
||||
func SanitizePath(path string) string {
|
||||
// If we're running in WSL, we need to convert Windows paths to Unix paths.
|
||||
// This is because the git binary can be invoked through `git.exe` and
|
||||
// therefore returns Windows paths.
|
||||
if os.Getenv("WSL_DISTRO_NAME") != "" && windowsPathRegex.MatchString(path) {
|
||||
unixPath := strings.ReplaceAll(path, "\\", "/")
|
||||
drive := strings.ToLower(string(unixPath[0]))
|
||||
rest := filepath.Clean(unixPath[3:])
|
||||
return filepath.Join("/mnt", drive, rest)
|
||||
}
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
|
@@ -6,14 +6,15 @@ package gitutil
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSanitizePathUnix(t *testing.T) {
|
||||
assert.Equal(t, "/home/foobar", SanitizePath("/home/foobar"))
|
||||
assert.Equal(t, "/home/foobar", osutil.SanitizePath("/home/foobar"))
|
||||
}
|
||||
|
||||
func TestSanitizePathWSL(t *testing.T) {
|
||||
t.Setenv("WSL_DISTRO_NAME", "Ubuntu")
|
||||
assert.Equal(t, "/mnt/c/Users/foobar", SanitizePath("C:\\Users\\foobar"))
|
||||
assert.Equal(t, "/mnt/c/Users/foobar", osutil.SanitizePath("C:\\Users\\foobar"))
|
||||
}
|
||||
|
@@ -2,13 +2,8 @@ package gitutil
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func gitPath(wd string) (string, error) {
|
||||
return exec.LookPath("git.exe")
|
||||
}
|
||||
|
||||
func SanitizePath(path string) string {
|
||||
return filepath.ToSlash(filepath.Clean(path))
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -12,7 +13,7 @@ func TestSanitizePathWindows(t *testing.T) {
|
||||
if isGitBash() {
|
||||
expected = "C:/Users/foobar"
|
||||
}
|
||||
assert.Equal(t, expected, SanitizePath("C:/Users/foobar"))
|
||||
assert.Equal(t, expected, osutil.SanitizePath("C:/Users/foobar"))
|
||||
}
|
||||
|
||||
func isGitBash() bool {
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/docker/buildx/version"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/metric/noop"
|
||||
@@ -86,6 +87,7 @@ func (m *MeterProvider) Report(ctx context.Context) {
|
||||
var rm metricdata.ResourceMetrics
|
||||
if err := m.reader.Collect(ctx, &rm); err != nil {
|
||||
// Error when collecting metrics. Do not send any.
|
||||
otel.Handle(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,7 +95,9 @@ func (m *MeterProvider) Report(ctx context.Context) {
|
||||
for _, exp := range m.exporters {
|
||||
exp := exp
|
||||
eg.Go(func() error {
|
||||
_ = exp.Export(ctx, &rm)
|
||||
if err := exp.Export(ctx, &rm); err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
_ = exp.Shutdown(ctx)
|
||||
return nil
|
||||
})
|
||||
|
33
util/metricutil/resource_test.go
Normal file
33
util/metricutil/resource_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package metricutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
func TestResource(t *testing.T) {
|
||||
setErrorHandler(t)
|
||||
|
||||
// Ensure resource creation doesn't result in an error.
|
||||
// This is because the schema urls for the various attributes need to be
|
||||
// the same, but it's really easy to import the wrong package when upgrading
|
||||
// otel to anew version and the buildx CLI swallows any visible errors.
|
||||
res := Resource()
|
||||
|
||||
// Ensure an attribute is present.
|
||||
assert.True(t, res.Set().HasValue("telemetry.sdk.version"), "resource attribute missing")
|
||||
}
|
||||
|
||||
func setErrorHandler(tb testing.TB) {
|
||||
tb.Helper()
|
||||
|
||||
errorHandler := otel.GetErrorHandler()
|
||||
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
|
||||
tb.Errorf("otel error: %s", err)
|
||||
}))
|
||||
tb.Cleanup(func() {
|
||||
otel.SetErrorHandler(errorHandler)
|
||||
})
|
||||
}
|
30
util/osutil/path.go
Normal file
30
util/osutil/path.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// GetWd retrieves the current working directory.
|
||||
//
|
||||
// On Windows, this function will return the long path name
|
||||
// version of the path.
|
||||
func GetWd() string {
|
||||
wd, _ := os.Getwd()
|
||||
if lp, err := GetLongPathName(wd); err == nil {
|
||||
return lp
|
||||
}
|
||||
return wd
|
||||
}
|
||||
|
||||
func IsLocalDir(c string) bool {
|
||||
st, err := os.Stat(c)
|
||||
return err == nil && st.IsDir()
|
||||
}
|
||||
|
||||
func ToAbs(path string) string {
|
||||
if !filepath.IsAbs(path) {
|
||||
path, _ = filepath.Abs(filepath.Join(GetWd(), path))
|
||||
}
|
||||
return SanitizePath(path)
|
||||
}
|
31
util/osutil/path_unix.go
Normal file
31
util/osutil/path_unix.go
Normal file
@@ -0,0 +1,31 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetLongPathName is a no-op on non-Windows platforms.
|
||||
func GetLongPathName(path string) (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
var windowsPathRegex = regexp.MustCompile(`^[A-Za-z]:[\\/].*$`)
|
||||
|
||||
func SanitizePath(path string) string {
|
||||
// If we're running in WSL, we need to convert Windows paths to Unix paths.
|
||||
// This is because the git binary can be invoked through `git.exe` and
|
||||
// therefore returns Windows paths.
|
||||
if os.Getenv("WSL_DISTRO_NAME") != "" && windowsPathRegex.MatchString(path) {
|
||||
unixPath := strings.ReplaceAll(path, "\\", "/")
|
||||
drive := strings.ToLower(string(unixPath[0]))
|
||||
rest := filepath.Clean(unixPath[3:])
|
||||
return filepath.Join("/mnt", drive, rest)
|
||||
}
|
||||
return filepath.Clean(path)
|
||||
}
|
34
util/osutil/path_windows.go
Normal file
34
util/osutil/path_windows.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// GetLongPathName converts Windows short pathnames to full pathnames.
|
||||
// For example C:\Users\ADMIN~1 --> C:\Users\Administrator.
|
||||
func GetLongPathName(path string) (string, error) {
|
||||
// See https://groups.google.com/forum/#!topic/golang-dev/1tufzkruoTg
|
||||
p, err := windows.UTF16FromString(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b := p // GetLongPathName says we can reuse buffer
|
||||
n, err := windows.GetLongPathName(&p[0], &b[0], uint32(len(b)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n > uint32(len(b)) {
|
||||
b = make([]uint16, n)
|
||||
_, err = windows.GetLongPathName(&p[0], &b[0], uint32(len(b)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return windows.UTF16ToString(b), nil
|
||||
}
|
||||
|
||||
func SanitizePath(path string) string {
|
||||
return filepath.ToSlash(filepath.Clean(path))
|
||||
}
|
Reference in New Issue
Block a user