2024-03-28 14:53:36 -04:00
|
|
|
// FIXME(jsternberg): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
2024-06-18 06:07:25 -04:00
|
|
|
//go:build go1.21
|
2024-03-28 14:53:36 -04:00
|
|
|
|
2024-02-20 10:42:33 -05:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-09-12 12:14:43 -04:00
|
|
|
"fmt"
|
|
|
|
"io/fs"
|
2024-02-20 10:42:33 -05:00
|
|
|
"net/url"
|
2024-03-28 12:22:53 -04:00
|
|
|
"os"
|
2024-02-20 10:42:33 -05:00
|
|
|
"path"
|
2024-09-12 12:14:43 -04:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"unicode"
|
2024-02-20 10:42:33 -05:00
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
|
|
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
|
|
|
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
|
|
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
|
|
)
|
|
|
|
|
2024-03-28 12:22:53 -04:00
|
|
|
const (
|
|
|
|
otelContextFieldName string = "otel"
|
|
|
|
otelExporterOTLPEndpoint string = "OTEL_EXPORTER_OTLP_ENDPOINT"
|
|
|
|
debugEnvVarPrefix string = "DOCKER_CLI_"
|
|
|
|
)
|
2024-02-20 10:42:33 -05:00
|
|
|
|
|
|
|
// dockerExporterOTLPEndpoint retrieves the OTLP endpoint used for the docker reporter
|
|
|
|
// from the current context.
|
|
|
|
func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) {
|
|
|
|
meta, err := cli.ContextStore().GetMetadata(cli.CurrentContext())
|
|
|
|
if err != nil {
|
|
|
|
otel.Handle(err)
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
var otelCfg any
|
|
|
|
switch m := meta.Metadata.(type) {
|
|
|
|
case DockerContext:
|
|
|
|
otelCfg = m.AdditionalFields[otelContextFieldName]
|
|
|
|
case map[string]any:
|
|
|
|
otelCfg = m[otelContextFieldName]
|
|
|
|
}
|
|
|
|
|
2024-04-17 10:32:41 -04:00
|
|
|
if otelCfg != nil {
|
|
|
|
otelMap, ok := otelCfg.(map[string]any)
|
|
|
|
if !ok {
|
|
|
|
otel.Handle(errors.Errorf(
|
|
|
|
"unexpected type for field %q: %T (expected: %T)",
|
|
|
|
otelContextFieldName,
|
|
|
|
otelCfg,
|
|
|
|
otelMap,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
|
|
|
|
endpoint, _ = otelMap[otelExporterOTLPEndpoint].(string)
|
2024-02-20 10:42:33 -05:00
|
|
|
}
|
|
|
|
|
2024-03-28 12:22:53 -04:00
|
|
|
// Override with env var value if it exists AND IS SET
|
|
|
|
// (ignore otel defaults for this override when the key exists but is empty)
|
|
|
|
if override := os.Getenv(debugEnvVarPrefix + otelExporterOTLPEndpoint); override != "" {
|
|
|
|
endpoint = override
|
|
|
|
}
|
|
|
|
|
|
|
|
if endpoint == "" {
|
2024-02-20 10:42:33 -05:00
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the endpoint. The docker config expects the endpoint to be
|
|
|
|
// in the form of a URL to match the environment variable, but this
|
|
|
|
// option doesn't correspond directly to WithEndpoint.
|
|
|
|
//
|
|
|
|
// We pretend we're the same as the environment reader.
|
|
|
|
u, err := url.Parse(endpoint)
|
|
|
|
if err != nil {
|
|
|
|
otel.Handle(errors.Errorf("docker otel endpoint is invalid: %s", err))
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
switch u.Scheme {
|
|
|
|
case "unix":
|
2024-09-12 12:14:43 -04:00
|
|
|
endpoint = unixSocketEndpoint(u)
|
2024-02-20 10:42:33 -05:00
|
|
|
case "https":
|
|
|
|
secure = true
|
|
|
|
fallthrough
|
|
|
|
case "http":
|
|
|
|
endpoint = path.Join(u.Host, u.Path)
|
|
|
|
}
|
|
|
|
return endpoint, secure
|
|
|
|
}
|
|
|
|
|
|
|
|
func dockerSpanExporter(ctx context.Context, cli Cli) []sdktrace.TracerProviderOption {
|
|
|
|
endpoint, secure := dockerExporterOTLPEndpoint(cli)
|
|
|
|
if endpoint == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
opts := []otlptracegrpc.Option{
|
|
|
|
otlptracegrpc.WithEndpoint(endpoint),
|
|
|
|
}
|
|
|
|
if !secure {
|
|
|
|
opts = append(opts, otlptracegrpc.WithInsecure())
|
|
|
|
}
|
|
|
|
|
|
|
|
exp, err := otlptracegrpc.New(ctx, opts...)
|
|
|
|
if err != nil {
|
|
|
|
otel.Handle(err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return []sdktrace.TracerProviderOption{sdktrace.WithBatcher(exp, sdktrace.WithExportTimeout(exportTimeout))}
|
|
|
|
}
|
|
|
|
|
|
|
|
func dockerMetricExporter(ctx context.Context, cli Cli) []sdkmetric.Option {
|
|
|
|
endpoint, secure := dockerExporterOTLPEndpoint(cli)
|
|
|
|
if endpoint == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
opts := []otlpmetricgrpc.Option{
|
|
|
|
otlpmetricgrpc.WithEndpoint(endpoint),
|
|
|
|
}
|
|
|
|
if !secure {
|
|
|
|
opts = append(opts, otlpmetricgrpc.WithInsecure())
|
|
|
|
}
|
|
|
|
|
|
|
|
exp, err := otlpmetricgrpc.New(ctx, opts...)
|
|
|
|
if err != nil {
|
|
|
|
otel.Handle(err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return []sdkmetric.Option{sdkmetric.WithReader(newCLIReader(exp))}
|
|
|
|
}
|
2024-09-12 12:14:43 -04:00
|
|
|
|
|
|
|
// unixSocketEndpoint converts the unix scheme from URL to
|
|
|
|
// an OTEL endpoint that can be used with the OTLP exporter.
|
|
|
|
//
|
|
|
|
// The OTLP exporter handles unix sockets in a strange way.
|
|
|
|
// It seems to imply they can be used as an environment variable
|
|
|
|
// and are handled properly, but they don't seem to be as the behavior
|
|
|
|
// of the environment variable is to strip the scheme from the endpoint
|
|
|
|
// while the underlying implementation needs the scheme to use the
|
|
|
|
// correct resolver.
|
|
|
|
func unixSocketEndpoint(u *url.URL) string {
|
|
|
|
// GRPC does not allow host to be used.
|
|
|
|
socketPath := u.Path
|
|
|
|
|
|
|
|
// If we are on windows and we have an absolute path
|
|
|
|
// that references a letter drive, check to see if the
|
|
|
|
// WSL equivalent path exists and we should use that instead.
|
|
|
|
if isWsl() {
|
|
|
|
if p := wslSocketPath(socketPath, os.DirFS("/")); p != "" {
|
|
|
|
socketPath = p
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Enforce that we are using forward slashes.
|
|
|
|
return "unix://" + filepath.ToSlash(socketPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// wslSocketPath will convert the referenced URL to a WSL-compatible
|
|
|
|
// path and check if that path exists. If the path exists, it will
|
|
|
|
// be returned.
|
|
|
|
func wslSocketPath(s string, f fs.FS) string {
|
|
|
|
if p := toWslPath(s); p != "" {
|
|
|
|
if _, err := stat(p, f); err == nil {
|
|
|
|
return "/" + p
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// toWslPath converts the referenced URL to a WSL-compatible
|
|
|
|
// path if this looks like a Windows absolute path.
|
|
|
|
//
|
|
|
|
// If no drive is in the URL, defaults to the C drive.
|
|
|
|
func toWslPath(s string) string {
|
|
|
|
drive, p, ok := parseUNCPath(s)
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
}
|
2024-09-17 11:51:30 -04:00
|
|
|
return fmt.Sprintf("mnt/%s%s", strings.ToLower(drive), p)
|
2024-09-12 12:14:43 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
func parseUNCPath(s string) (drive, p string, ok bool) {
|
|
|
|
// UNC paths use backslashes but we're using forward slashes
|
|
|
|
// so also enforce that here.
|
|
|
|
//
|
|
|
|
// In reality, this should have been enforced much earlier
|
|
|
|
// than here since backslashes aren't allowed in URLs, but
|
|
|
|
// we're going to code defensively here.
|
|
|
|
s = filepath.ToSlash(s)
|
|
|
|
|
|
|
|
const uncPrefix = "//./"
|
|
|
|
if !strings.HasPrefix(s, uncPrefix) {
|
|
|
|
// Not a UNC path.
|
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
s = s[len(uncPrefix):]
|
|
|
|
|
|
|
|
parts := strings.SplitN(s, "/", 2)
|
|
|
|
if len(parts) != 2 {
|
|
|
|
// Not enough components.
|
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
drive, ok = splitWindowsDrive(parts[0])
|
|
|
|
if !ok {
|
|
|
|
// Not a windows drive.
|
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
return drive, "/" + parts[1], true
|
|
|
|
}
|
|
|
|
|
|
|
|
// splitWindowsDrive checks if the string references a windows
|
|
|
|
// drive (such as c:) and returns the drive letter if it is.
|
|
|
|
func splitWindowsDrive(s string) (string, bool) {
|
|
|
|
if b := []rune(s); len(b) == 2 && unicode.IsLetter(b[0]) && b[1] == ':' {
|
|
|
|
return string(b[0]), true
|
|
|
|
}
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
|
|
|
|
func stat(p string, f fs.FS) (fs.FileInfo, error) {
|
|
|
|
if f, ok := f.(fs.StatFS); ok {
|
|
|
|
return f.Stat(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
file, err := f.Open(p)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer file.Close()
|
|
|
|
return file.Stat()
|
|
|
|
}
|
|
|
|
|
|
|
|
func isWsl() bool {
|
|
|
|
return os.Getenv("WSL_DISTRO_NAME") != ""
|
|
|
|
}
|