[OpenTelemetry] Add traces to init command (#2665)

Signed-off-by: James Humphries <james@james-humphries.co.uk>
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
Co-authored-by: Christian Mesh <christianmesh1@gmail.com>
This commit is contained in:
James Humphries
2025-04-25 12:40:48 +01:00
committed by GitHub
parent 7fa40db6af
commit d92d4f9c11
23 changed files with 476 additions and 192 deletions

View File

@@ -21,6 +21,7 @@ NEW FEATURES:
- Added support for S3 native locking ([#599](https://github.com/opentofu/opentofu/issues/599)) - Added support for S3 native locking ([#599](https://github.com/opentofu/opentofu/issues/599))
- Backend `pg` now allows the `table_name` and `index_name` to be specified. This enables a single database schema to support multiple backends via multiple tables. ([#2465](https://github.com/opentofu/opentofu/pull/2465)) - Backend `pg` now allows the `table_name` and `index_name` to be specified. This enables a single database schema to support multiple backends via multiple tables. ([#2465](https://github.com/opentofu/opentofu/pull/2465))
- Module variables and outputs can now be marked as `deprecated` to indicate their removal in the future. ([#1005](https://github.com/opentofu/opentofu/issues/1005)) - Module variables and outputs can now be marked as `deprecated` to indicate their removal in the future. ([#1005](https://github.com/opentofu/opentofu/issues/1005))
- OpenTelemetry tracing has been added to the `init` command for provider installation. Note: This feature is experimental and subject to change in the future. ([#2665](https://github.com/opentofu/opentofu/pull/2665))
ENHANCEMENTS: ENHANCEMENTS:

View File

@@ -15,13 +15,14 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"time"
"github.com/apparentlymart/go-shquot/shquot"
"github.com/hashicorp/go-plugin" "github.com/hashicorp/go-plugin"
"github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform-svchost/disco"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/command/cliconfig" "github.com/opentofu/opentofu/internal/command/cliconfig"
"github.com/opentofu/opentofu/internal/command/format" "github.com/opentofu/opentofu/internal/command/format"
@@ -29,8 +30,8 @@ import (
"github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/internal/terminal" "github.com/opentofu/opentofu/internal/terminal"
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/version" "github.com/opentofu/opentofu/version"
"go.opentelemetry.io/otel/trace"
backendInit "github.com/opentofu/opentofu/internal/backend/init" backendInit "github.com/opentofu/opentofu/internal/backend/init"
) )
@@ -69,25 +70,22 @@ func main() {
func realMain() int { func realMain() int {
defer logging.PanicHandler() defer logging.PanicHandler()
var err error err := tracing.OpenTelemetryInit()
err = openTelemetryInit()
if err != nil { if err != nil {
// openTelemetryInit can only fail if OpenTofu was run with an // openTelemetryInit can only fail if OpenTofu was run with an
// explicit environment variable to enable telemetry collection, // explicit environment variable to enable telemetry collection,
// so in typical use we cannot get here. // so in typical use we cannot get here.
Ui.Error(fmt.Sprintf("Could not initialize telemetry: %s", err)) Ui.Error(fmt.Sprintf("Could not initialize telemetry: %s", err))
Ui.Error(fmt.Sprintf("Unset environment variable %s if you don't intend to collect telemetry from OpenTofu.", openTelemetryExporterEnvVar)) Ui.Error(fmt.Sprintf("Unset environment variable %s if you don't intend to collect telemetry from OpenTofu.", tracing.OTELExporterEnvVar))
return 1 return 1
} }
var ctx context.Context defer tracing.ForceFlush(5 * time.Second)
var otelSpan trace.Span ctx := context.Background()
{
// At minimum we emit a span covering the entire command execution. // At minimum, we emit a span covering the entire command execution.
_, displayArgs := shquot.POSIXShellSplit(os.Args) ctx, span := tracing.Tracer().Start(ctx, "tofu")
ctx, otelSpan = tracer.Start(context.Background(), fmt.Sprintf("tofu %s", displayArgs)) defer span.End()
defer otelSpan.End()
}
tmpLogPath := os.Getenv(envTmpLogPath) tmpLogPath := os.Getenv(envTmpLogPath)
if tmpLogPath != "" { if tmpLogPath != "" {
@@ -102,9 +100,7 @@ func realMain() int {
} }
} }
log.Printf( log.Printf("[INFO] OpenTofu version: %s %s", Version, VersionPrerelease)
"[INFO] OpenTofu version: %s %s",
Version, VersionPrerelease)
for _, depMod := range version.InterestingDependencies() { for _, depMod := range version.InterestingDependencies() {
log.Printf("[DEBUG] using %s %s", depMod.Path, depMod.Version) log.Printf("[DEBUG] using %s %s", depMod.Path, depMod.Version)
} }
@@ -117,6 +113,7 @@ func realMain() int {
streams, err := terminal.Init() streams, err := terminal.Init()
if err != nil { if err != nil {
Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err)) Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err))
return 1 return 1
} }
if streams.Stdout.IsTerminal() { if streams.Stdout.IsTerminal() {
@@ -140,7 +137,7 @@ func realMain() int {
// path in the TERRAFORM_CONFIG_FILE environment variable (though probably // path in the TERRAFORM_CONFIG_FILE environment variable (though probably
// ill-advised) will be resolved relative to the true working directory, // ill-advised) will be resolved relative to the true working directory,
// not the overridden one. // not the overridden one.
config, diags := cliconfig.LoadConfig() config, diags := cliconfig.LoadConfig(ctx)
if len(diags) > 0 { if len(diags) > 0 {
// Since we haven't instantiated a command.Meta yet, we need to do // Since we haven't instantiated a command.Meta yet, we need to do

View File

@@ -1,93 +0,0 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package main
import (
"context"
"os"
"go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/version"
)
// If this environment variable is set to "otlp" when running OpenTofu CLI
// then we'll enable an experimental OTLP trace exporter.
//
// BEWARE! This is not a committed external interface.
//
// Everything about this is experimental and subject to change in future
// releases. Do not depend on anything about the structure of this output.
// This mechanism might be removed altogether if a different strategy seems
// better based on experience with this experiment.
const openTelemetryExporterEnvVar = "OTEL_TRACES_EXPORTER"
// tracer is the OpenTelemetry tracer to use for traces in package main only.
var tracer trace.Tracer
func init() {
tracer = otel.Tracer("github.com/opentofu/opentofu")
}
// openTelemetryInit initializes the optional OpenTelemetry exporter.
//
// By default we don't export telemetry information at all, since OpenTofu is
// a CLI tool and so we don't assume we're running in an environment with
// a telemetry collector available.
//
// However, for those running OpenTofu in automation we allow setting
// the standard OpenTelemetry environment variable OTEL_TRACES_EXPORTER=otlp
// to enable an OTLP exporter, which is in turn configured by all of the
// standard OTLP exporter environment variables:
//
// https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options
//
// We don't currently support any other telemetry export protocols, because
// OTLP has emerged as a de-facto standard and each other exporter we support
// means another relatively-heavy external dependency. OTLP happens to use
// protocol buffers and gRPC, which OpenTofu would depend on for other reasons
// anyway.
func openTelemetryInit() error {
// We'll check the environment variable ourselves first, because the
// "autoexport" helper we're about to use is built under the assumption
// that exporting should always be enabled and so will expect to find
// an OTLP server on localhost if no environment variables are set at all.
if os.Getenv(openTelemetryExporterEnvVar) != "otlp" {
return nil // By default we just discard all telemetry calls
}
otelResource := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("OpenTofu CLI"),
semconv.ServiceVersionKey.String(version.Version),
)
// If the environment variable was set to explicitly enable telemetry
// then we'll enable it, using the "autoexport" library to automatically
// handle the details based on the other OpenTelemetry standard environment
// variables.
exp, err := autoexport.NewSpanExporter(context.Background())
if err != nil {
return err
}
sp := sdktrace.NewSimpleSpanProcessor(exp)
provider := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(sp),
sdktrace.WithResource(otelResource),
)
otel.SetTracerProvider(provider)
pgtr := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
otel.SetTextMapPropagator(pgtr)
return nil
}

14
go.mod
View File

@@ -34,9 +34,10 @@ require (
github.com/cli/browser v1.3.0 github.com/cli/browser v1.3.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/dylanmei/winrmtest v0.0.0-20210303004826-fbc9ae56efb6 github.com/dylanmei/winrmtest v0.0.0-20210303004826-fbc9ae56efb6
github.com/go-logr/stdr v1.2.2
github.com/go-test/deep v1.1.0 github.com/go-test/deep v1.1.0
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1
github.com/google/go-cmp v0.6.0 github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/googleapis/gax-go/v2 v2.12.0 github.com/googleapis/gax-go/v2 v2.12.0
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62 github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62
@@ -93,9 +94,10 @@ require (
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940
github.com/zclconf/go-cty-yaml v1.1.0 github.com/zclconf/go-cty-yaml v1.1.0
go.opentelemetry.io/contrib/exporters/autoexport v0.0.0-20230703072336-9a582bd098a2 go.opentelemetry.io/contrib/exporters/autoexport v0.0.0-20230703072336-9a582bd098a2
go.opentelemetry.io/otel v1.34.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1
go.opentelemetry.io/otel/sdk v1.33.0 go.opentelemetry.io/otel v1.35.0
go.opentelemetry.io/otel/trace v1.34.0 go.opentelemetry.io/otel/sdk v1.35.0
go.opentelemetry.io/otel/trace v1.35.0
go.uber.org/mock v0.4.0 go.uber.org/mock v0.4.0
golang.org/x/crypto v0.35.0 golang.org/x/crypto v0.35.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/exp v0.0.0-20230905200255-921286631fa9
@@ -180,7 +182,6 @@ require (
github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/errors v0.20.2 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/gofrs/uuid v4.0.0+incompatible // indirect
@@ -252,11 +253,10 @@ require (
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0 // indirect go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.34.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/sync v0.11.0 // indirect golang.org/x/sync v0.11.0 // indirect

20
go.sum
View File

@@ -567,8 +567,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI=
github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -1088,8 +1088,8 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
@@ -1098,12 +1098,12 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkE
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 h1:+XWJd3jf75RXJq29mxbuXhCXFDG3S3R4vBUeSI2P7tE= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 h1:+XWJd3jf75RXJq29mxbuXhCXFDG3S3R4vBUeSI2P7tE=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0/go.mod h1:hqgzBPTf4yONMFgdZvL/bK42R/iinTyVQtiWihs3SZc= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0/go.mod h1:hqgzBPTf4yONMFgdZvL/bK42R/iinTyVQtiWihs3SZc=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=

View File

@@ -14,6 +14,7 @@
package cliconfig package cliconfig
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
@@ -23,7 +24,6 @@ import (
"strings" "strings"
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
svchost "github.com/hashicorp/terraform-svchost" svchost "github.com/hashicorp/terraform-svchost"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
@@ -116,7 +116,7 @@ func DataDirs() ([]string, error) {
// LoadConfig reads the CLI configuration from the various filesystem locations // LoadConfig reads the CLI configuration from the various filesystem locations
// and from the environment, returning a merged configuration along with any // and from the environment, returning a merged configuration along with any
// diagnostics (errors and warnings) encountered along the way. // diagnostics (errors and warnings) encountered along the way.
func LoadConfig() (*Config, tfdiags.Diagnostics) { func LoadConfig(_ context.Context) (*Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
configVal := BuiltinConfig // copy configVal := BuiltinConfig // copy
config := &configVal config := &configVal

View File

@@ -6,6 +6,7 @@
package cliconfig package cliconfig
import ( import (
"context"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -13,6 +14,7 @@ import (
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
) )
@@ -66,7 +68,7 @@ func TestLoadConfig_non_existing_file(t *testing.T) {
t.Setenv("TF_CLI_CONFIG_FILE", cliTmpFile) t.Setenv("TF_CLI_CONFIG_FILE", cliTmpFile)
c, errs := LoadConfig() c, errs := LoadConfig(context.Background())
if errs.HasErrors() || c.Validate().HasErrors() { if errs.HasErrors() || c.Validate().HasErrors() {
t.Fatalf("err: %s", errs) t.Fatalf("err: %s", errs)
} }

View File

@@ -17,8 +17,7 @@ import (
svchost "github.com/hashicorp/terraform-svchost" svchost "github.com/hashicorp/terraform-svchost"
"github.com/posener/complete" "github.com/posener/complete"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"go.opentelemetry.io/otel/attribute" otelAttr "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
@@ -36,6 +35,7 @@ import (
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tofu" "github.com/opentofu/opentofu/internal/tofu"
"github.com/opentofu/opentofu/internal/tofumigrate" "github.com/opentofu/opentofu/internal/tofumigrate"
"github.com/opentofu/opentofu/internal/tracing"
tfversion "github.com/opentofu/opentofu/version" tfversion "github.com/opentofu/opentofu/version"
) )
@@ -46,6 +46,11 @@ type InitCommand struct {
} }
func (c *InitCommand) Run(args []string) int { func (c *InitCommand) Run(args []string) int {
ctx := c.CommandContext()
ctx, span := tracing.Tracer().Start(ctx, "Init")
defer span.End()
var flagFromModule, flagLockfile, testsDirectory string var flagFromModule, flagLockfile, testsDirectory string
var flagBackend, flagCloud, flagGet, flagUpgrade bool var flagBackend, flagCloud, flagGet, flagUpgrade bool
var flagPluginPath FlagStringSlice var flagPluginPath FlagStringSlice
@@ -129,7 +134,7 @@ func (c *InitCommand) Run(args []string) int {
} }
// Initialization can be aborted by interruption signals // Initialization can be aborted by interruption signals
ctx, done := c.InterruptibleContext(c.CommandContext()) ctx, done := c.InterruptibleContext(ctx)
defer done() defer done()
// This will track whether we outputted anything so that we know whether // This will track whether we outputted anything so that we know whether
@@ -159,19 +164,19 @@ func (c *InitCommand) Run(args []string) int {
ShowLocalPaths: false, // since they are in a weird location for init ShowLocalPaths: false, // since they are in a weird location for init
} }
ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes( ctx, span := tracing.Tracer().Start(ctx, "From module", trace.WithAttributes(
attribute.String("module_source", src), otelAttr.String("opentofu.module_source", src),
)) ))
defer span.End()
initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks) initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks)
diags = diags.Append(initDirFromModuleDiags) diags = diags.Append(initDirFromModuleDiags)
if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() { if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() {
c.showDiagnostics(diags) c.showDiagnostics(diags)
span.SetStatus(codes.Error, "module installation failed") tracing.SetSpanError(span, initDirFromModuleDiags)
span.End() span.End()
return 1 return 1
} }
span.End()
c.Ui.Output("") c.Ui.Output("")
} }
@@ -402,8 +407,8 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear
return false, false, nil return false, false, nil
} }
ctx, span := tracer.Start(ctx, "install modules", trace.WithAttributes( ctx, span := tracing.Tracer().Start(ctx, "Get modules", trace.WithAttributes(
attribute.Bool("upgrade", upgrade), otelAttr.Bool("opentofu.modules.upgrade", upgrade),
)) ))
defer span.End() defer span.End()
@@ -442,7 +447,7 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear
} }
func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig rawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "initialize cloud backend") ctx, span := tracing.Tracer().Start(ctx, "Cloud backend init")
_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here _ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
defer span.End() defer span.End()
@@ -470,7 +475,7 @@ func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extra
} }
func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig rawFlags, enc encryption.Encryption) (be backend.Backend, output bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "initialize backend") ctx, span := tracing.Tracer().Start(ctx, "Backend init")
_ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here _ = ctx // prevent staticcheck from complaining to avoid a maintenance hazard of having the wrong ctx in scope here
defer span.End() defer span.End()
@@ -555,7 +560,7 @@ the backend configuration is present and valid.
// Load the complete module tree, and fetch any missing providers. // Load the complete module tree, and fetch any missing providers.
// This method outputs its own Ui. // This method outputs its own Ui.
func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string) (output, abort bool, diags tfdiags.Diagnostics) { func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string) (output, abort bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install providers") ctx, span := tracing.Tracer().Start(ctx, "Get Providers")
defer span.End() defer span.End()
// Dev overrides cause the result of "tofu init" to be irrelevant for // Dev overrides cause the result of "tofu init" to be irrelevant for
@@ -1035,7 +1040,7 @@ in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.`)) version control system if they represent changes you intended to make.`))
} }
moreDiags = c.replaceLockedDependencies(newLocks) moreDiags = c.replaceLockedDependencies(ctx, newLocks)
diags = diags.Append(moreDiags) diags = diags.Append(moreDiags)
} }

View File

@@ -17,8 +17,6 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/convert"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs" "github.com/opentofu/opentofu/internal/configs"
@@ -275,9 +273,6 @@ func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) {
// this package has a reasonable implementation for displaying notifications // this package has a reasonable implementation for displaying notifications
// via a provided cli.Ui. // via a provided cli.Ui.
func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upgrade, installErrsOnly bool, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) { func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upgrade, installErrsOnly bool, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "install modules")
defer span.End()
rootDir = m.normalizePath(rootDir) rootDir = m.normalizePath(rootDir)
err := os.MkdirAll(m.modulesDir(), os.ModePerm) err := os.MkdirAll(m.modulesDir(), os.ModePerm)
@@ -322,11 +317,6 @@ func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upg
// this package has a reasonable implementation for displaying notifications // this package has a reasonable implementation for displaying notifications
// via a provided cli.Ui. // via a provided cli.Ui.
func (m *Meta) initDirFromModule(ctx context.Context, targetDir string, addr string, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) { func (m *Meta) initDirFromModule(ctx context.Context, targetDir string, addr string, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) {
ctx, span := tracer.Start(ctx, "initialize directory from module", trace.WithAttributes(
attribute.String("source_addr", addr),
))
defer span.End()
loader, err := m.initConfigLoader() loader, err := m.initConfigLoader()
if err != nil { if err != nil {
diags = diags.Append(err) diags = diags.Append(err)

View File

@@ -6,6 +6,7 @@
package command package command
import ( import (
"context"
"log" "log"
"os" "os"
@@ -64,8 +65,8 @@ func (m *Meta) lockedDependencies() (*depsfile.Locks, tfdiags.Diagnostics) {
// replaceLockedDependencies creates or overwrites the lock file in the // replaceLockedDependencies creates or overwrites the lock file in the
// current working directory to contain the information recorded in the given // current working directory to contain the information recorded in the given
// locks object. // locks object.
func (m *Meta) replaceLockedDependencies(new *depsfile.Locks) tfdiags.Diagnostics { func (m *Meta) replaceLockedDependencies(ctx context.Context, new *depsfile.Locks) tfdiags.Diagnostics {
return depsfile.SaveLocksToFile(new, dependencyLockFilename) return depsfile.SaveLocksToFile(ctx, new, dependencyLockFilename)
} }
// annotateDependencyLocksWithOverrides modifies the given Locks object in-place // annotateDependencyLocksWithOverrides modifies the given Locks object in-place

View File

@@ -331,7 +331,7 @@ func (c *ProvidersLockCommand) Run(args []string) int {
newLocks.SetProvider(provider, version, constraints, hashes) newLocks.SetProvider(provider, version, constraints, hashes)
} }
moreDiags = c.replaceLockedDependencies(newLocks) moreDiags = c.replaceLockedDependencies(ctx, newLocks)
diags = diags.Append(moreDiags) diags = diags.Append(moreDiags)
c.showDiagnostics(diags) c.showDiagnostics(diags)

View File

@@ -1,17 +0,0 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package command
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
var tracer trace.Tracer
func init() {
tracer = otel.Tracer("github.com/opentofu/opentofu/internal/command")
}

View File

@@ -6,6 +6,7 @@
package command package command
import ( import (
"context"
"strings" "strings"
"testing" "testing"
@@ -51,7 +52,7 @@ func TestVersion(t *testing.T) {
VersionPrerelease: "foo", VersionPrerelease: "foo",
Platform: getproviders.Platform{OS: "aros", Arch: "riscv64"}, Platform: getproviders.Platform{OS: "aros", Arch: "riscv64"},
} }
if err := c.replaceLockedDependencies(locks); err != nil { if err := c.replaceLockedDependencies(context.Background(), locks); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if code := c.Run([]string{}); code != 0 { if code := c.Run([]string{}); code != 0 {
@@ -149,7 +150,7 @@ func TestVersion_json(t *testing.T) {
VersionPrerelease: "foo", VersionPrerelease: "foo",
Platform: getproviders.Platform{OS: "aros", Arch: "riscv64"}, Platform: getproviders.Platform{OS: "aros", Arch: "riscv64"},
} }
if err := c.replaceLockedDependencies(locks); err != nil { if err := c.replaceLockedDependencies(context.Background(), locks); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if code := c.Run([]string{"-json"}); code != 0 { if code := c.Run([]string{"-json"}); code != 0 {

View File

@@ -6,6 +6,7 @@
package depsfile package depsfile
import ( import (
"context"
"fmt" "fmt"
"sort" "sort"
@@ -15,11 +16,14 @@ import (
"github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hclwrite" "github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/replacefile" "github.com/opentofu/opentofu/internal/replacefile"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/version" "github.com/opentofu/opentofu/version"
) )
@@ -90,12 +94,19 @@ func loadLocks(loadParse func(*hclparse.Parser) (*hcl.File, hcl.Diagnostics)) (*
// the file as a signal to invalidate cached metadata. Consequently, other // the file as a signal to invalidate cached metadata. Consequently, other
// temporary files may be temporarily created in the same directory as the // temporary files may be temporarily created in the same directory as the
// given filename during the operation. // given filename during the operation.
func SaveLocksToFile(locks *Locks, filename string) tfdiags.Diagnostics { func SaveLocksToFile(ctx context.Context, locks *Locks, filename string) tfdiags.Diagnostics {
_, span := tracing.Tracer().Start(ctx, "Save lockfile", trace.WithAttributes(semconv.FileName(filename)))
defer span.End()
src, diags := SaveLocksToBytes(locks) src, diags := SaveLocksToBytes(locks)
if diags.HasErrors() { if diags.HasErrors() {
tracing.SetSpanError(span, diags)
return diags return diags
} }
span.AddEvent("Serialized lockfile")
span.SetAttributes(semconv.FileSize(len(src)))
err := replacefile.AtomicWriteFile(filename, src, 0644) err := replacefile.AtomicWriteFile(filename, src, 0644)
if err != nil { if err != nil {
diags = diags.Append(tfdiags.Sourceless( diags = diags.Append(tfdiags.Sourceless(
@@ -103,6 +114,7 @@ func SaveLocksToFile(locks *Locks, filename string) tfdiags.Diagnostics {
"Failed to update dependency lock file", "Failed to update dependency lock file",
fmt.Sprintf("Error while writing new dependency lock information to %s: %s.", filename, err), fmt.Sprintf("Error while writing new dependency lock information to %s: %s.", filename, err),
)) ))
tracing.SetSpanError(span, diags)
return diags return diags
} }

View File

@@ -7,12 +7,14 @@ package depsfile
import ( import (
"bufio" "bufio"
"context"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/tfdiags" "github.com/opentofu/opentofu/internal/tfdiags"
@@ -227,7 +229,7 @@ func TestSaveLocksToFile(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
filename := filepath.Join(dir, LockFilePath) filename := filepath.Join(dir, LockFilePath)
diags := SaveLocksToFile(locks, filename) diags := SaveLocksToFile(context.Background(), locks, filename)
if diags.HasErrors() { if diags.HasErrors() {
t.Fatalf("unexpected errors\n%s", diags.Err().Error()) t.Fatalf("unexpected errors\n%s", diags.Err().Error())
} }

View File

@@ -15,8 +15,13 @@ import (
"github.com/hashicorp/go-getter" "github.com/hashicorp/go-getter"
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/internal/tracing"
) )
// PackageHTTPURL is a provider package location accessible via HTTP. // PackageHTTPURL is a provider package location accessible via HTTP.
@@ -33,6 +38,11 @@ func (p PackageHTTPURL) String() string { return string(p) }
func (p PackageHTTPURL) InstallProviderPackage(ctx context.Context, meta PackageMeta, targetDir string, allowedHashes []Hash) (*PackageAuthenticationResult, error) { func (p PackageHTTPURL) InstallProviderPackage(ctx context.Context, meta PackageMeta, targetDir string, allowedHashes []Hash) (*PackageAuthenticationResult, error) {
url := meta.Location.String() url := meta.Location.String()
ctx, span := tracing.Tracer().Start(ctx, "Install (http)", trace.WithAttributes(
semconv.URLFull(url),
))
defer span.End()
// When we're installing from an HTTP URL we expect the URL to refer to // When we're installing from an HTTP URL we expect the URL to refer to
// a zip file. We'll fetch that into a temporary file here and then // a zip file. We'll fetch that into a temporary file here and then
// delegate to installFromLocalArchive below to actually extract it. // delegate to installFromLocalArchive below to actually extract it.
@@ -43,6 +53,7 @@ func (p PackageHTTPURL) InstallProviderPackage(ctx context.Context, meta Package
retryableClient := retryablehttp.NewClient() retryableClient := retryablehttp.NewClient()
retryableClient.HTTPClient = httpclient.New() retryableClient.HTTPClient = httpclient.New()
retryableClient.HTTPClient.Transport = otelhttp.NewTransport(retryableClient.HTTPClient.Transport)
retryableClient.RetryMax = maxHTTPPackageRetryCount retryableClient.RetryMax = maxHTTPPackageRetryCount
retryableClient.RequestLogHook = func(logger retryablehttp.Logger, _ *http.Request, i int) { retryableClient.RequestLogHook = func(logger retryablehttp.Logger, _ *http.Request, i int) {
if i > 0 { if i > 0 {

View File

@@ -10,6 +10,9 @@ import (
"fmt" "fmt"
"github.com/hashicorp/go-getter" "github.com/hashicorp/go-getter"
semconv "go.opentelemetry.io/otel/semconv/v1.30.0"
"github.com/opentofu/opentofu/internal/tracing"
) )
// We borrow the "unpack a zip file into a target directory" logic from // We borrow the "unpack a zip file into a target directory" logic from
@@ -31,7 +34,10 @@ var _ PackageLocation = PackageLocalArchive("")
func (p PackageLocalArchive) String() string { return string(p) } func (p PackageLocalArchive) String() string { return string(p) }
func (p PackageLocalArchive) InstallProviderPackage(_ context.Context, meta PackageMeta, targetDir string, allowedHashes []Hash) (*PackageAuthenticationResult, error) { func (p PackageLocalArchive) InstallProviderPackage(ctx context.Context, meta PackageMeta, targetDir string, allowedHashes []Hash) (*PackageAuthenticationResult, error) {
_, span := tracing.Tracer().Start(ctx, "Decompress (local archive)")
defer span.End()
var authResult *PackageAuthenticationResult var authResult *PackageAuthenticationResult
if meta.Authentication != nil { if meta.Authentication != nil {
var err error var err error
@@ -42,19 +48,24 @@ func (p PackageLocalArchive) InstallProviderPackage(_ context.Context, meta Pack
if len(allowedHashes) > 0 { if len(allowedHashes) > 0 {
if matches, err := meta.MatchesAnyHash(allowedHashes); err != nil { if matches, err := meta.MatchesAnyHash(allowedHashes); err != nil {
return authResult, fmt.Errorf( err := fmt.Errorf(
"failed to calculate checksum for %s %s package at %s: %w", "failed to calculate checksum for %s %s package at %s: %w",
meta.Provider, meta.Version, meta.Location, err, meta.Provider, meta.Version, meta.Location, err,
) )
tracing.SetSpanError(span, err)
return authResult, err
} else if !matches { } else if !matches {
return authResult, fmt.Errorf( err := fmt.Errorf(
"the current package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file; for more information: https://opentofu.org/docs/language/files/dependency-lock/#checksum-verification", "the current package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file; for more information: https://opentofu.org/docs/language/files/dependency-lock/#checksum-verification",
meta.Provider, meta.Version, meta.Provider, meta.Version,
) )
tracing.SetSpanError(span, err)
return authResult, err
} }
} }
filename := meta.Location.String() filename := meta.Location.String()
span.SetAttributes(semconv.FilePath(filename))
// NOTE: We're not checking whether there's already a directory at // NOTE: We're not checking whether there's already a directory at
// targetDir with some files in it. Packages are supposed to be immutable // targetDir with some files in it. Packages are supposed to be immutable
@@ -67,6 +78,7 @@ func (p PackageLocalArchive) InstallProviderPackage(_ context.Context, meta Pack
//nolint:mnd // magic number predates us using this linter //nolint:mnd // magic number predates us using this linter
err := unzip.Decompress(targetDir, filename, true, 0000) err := unzip.Decompress(targetDir, filename, true, 0000)
if err != nil { if err != nil {
tracing.SetSpanError(span, err)
return authResult, err return authResult, err
} }

View File

@@ -24,10 +24,16 @@ import (
"github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/go-retryablehttp"
svchost "github.com/hashicorp/terraform-svchost" svchost "github.com/hashicorp/terraform-svchost"
svcauth "github.com/hashicorp/terraform-svchost/auth" svcauth "github.com/hashicorp/terraform-svchost/auth"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
otelAttr "go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/httpclient" "github.com/opentofu/opentofu/internal/httpclient"
"github.com/opentofu/opentofu/internal/logging" "github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
"github.com/opentofu/opentofu/version" "github.com/opentofu/opentofu/version"
) )
@@ -82,6 +88,8 @@ func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registr
retryableClient.RequestLogHook = requestLogHook retryableClient.RequestLogHook = requestLogHook
retryableClient.ErrorHandler = maxRetryErrorHandler retryableClient.ErrorHandler = maxRetryErrorHandler
retryableClient.HTTPClient.Transport = otelhttp.NewTransport(retryableClient.HTTPClient.Transport)
retryableClient.Logger = log.New(logging.LogOutput(), "", log.Flags()) retryableClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
return &registryClient{ return &registryClient{
@@ -99,6 +107,13 @@ func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registr
// ErrUnauthorized if the registry responds with 401 or 403 status codes, or // ErrUnauthorized if the registry responds with 401 or 403 status codes, or
// ErrQueryFailed for any other protocol or operational problem. // ErrQueryFailed for any other protocol or operational problem.
func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provider) (map[string][]string, []string, error) { func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provider) (map[string][]string, []string, error) {
ctx, span := tracing.Tracer().Start(ctx,
"List Versions",
trace.WithAttributes(
otelAttr.String(traceattrs.ProviderAddress, addr.String()),
),
)
defer span.End()
endpointPath, err := url.Parse(path.Join(addr.Namespace, addr.Type, "versions")) endpointPath, err := url.Parse(path.Join(addr.Namespace, addr.Type, "versions"))
if err != nil { if err != nil {
// Should never happen because we're constructing this from // Should never happen because we're constructing this from
@@ -106,6 +121,7 @@ func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provid
return nil, nil, err return nil, nil, err
} }
endpointURL := c.baseURL.ResolveReference(endpointPath) endpointURL := c.baseURL.ResolveReference(endpointPath)
span.SetAttributes(semconv.URLFull(endpointURL.String()))
req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil) req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -115,7 +131,9 @@ func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provid
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return nil, nil, c.errQueryFailed(addr, err) errResult := c.errQueryFailed(addr, err)
tracing.SetSpanError(span, errResult)
return nil, nil, errResult
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -123,13 +141,19 @@ func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provid
case http.StatusOK: case http.StatusOK:
// Great! // Great!
case http.StatusNotFound: case http.StatusNotFound:
return nil, nil, ErrRegistryProviderNotKnown{ err := ErrRegistryProviderNotKnown{
Provider: addr, Provider: addr,
} }
tracing.SetSpanError(span, err)
return nil, nil, err
case http.StatusUnauthorized, http.StatusForbidden: case http.StatusUnauthorized, http.StatusForbidden:
return nil, nil, c.errUnauthorized(addr.Hostname) err := c.errUnauthorized(addr.Hostname)
tracing.SetSpanError(span, err)
return nil, nil, err
default: default:
return nil, nil, c.errQueryFailed(addr, errors.New(resp.Status)) err := c.errQueryFailed(addr, errors.New(resp.Status))
tracing.SetSpanError(span, err)
return nil, nil, err
} }
// We ignore the platforms portion of the response body, because the // We ignore the platforms portion of the response body, because the
@@ -146,7 +170,9 @@ func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provid
dec := json.NewDecoder(resp.Body) dec := json.NewDecoder(resp.Body)
if err := dec.Decode(&body); err != nil { if err := dec.Decode(&body); err != nil {
return nil, nil, c.errQueryFailed(addr, err) errResult := c.errQueryFailed(addr, err)
tracing.SetSpanError(span, errResult)
return nil, nil, errResult
} }
if len(body.Versions) == 0 { if len(body.Versions) == 0 {
@@ -181,12 +207,23 @@ func (c *registryClient) PackageMeta(ctx context.Context, provider addrs.Provide
target.OS, target.OS,
target.Arch, target.Arch,
)) ))
ctx, span := tracing.Tracer().Start(ctx,
"Fetch metadata",
trace.WithAttributes(
otelAttr.String(traceattrs.ProviderAddress, provider.String()),
otelAttr.String(traceattrs.ProviderVersion, version.String()),
))
defer span.End()
if err != nil { if err != nil {
// Should never happen because we're constructing this from // Should never happen because we're constructing this from
// already-validated components. // already-validated components.
return PackageMeta{}, err return PackageMeta{}, err
} }
endpointURL := c.baseURL.ResolveReference(endpointPath) endpointURL := c.baseURL.ResolveReference(endpointPath)
span.SetAttributes(
semconv.URLFull(endpointURL.String()),
)
req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil) req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
if err != nil { if err != nil {
@@ -197,6 +234,7 @@ func (c *registryClient) PackageMeta(ctx context.Context, provider addrs.Provide
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
tracing.SetSpanError(span, err)
return PackageMeta{}, c.errQueryFailed(provider, err) return PackageMeta{}, c.errQueryFailed(provider, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@@ -328,7 +366,7 @@ func (c *registryClient) PackageMeta(ctx context.Context, provider addrs.Provide
if shasumsURL.Scheme != "http" && shasumsURL.Scheme != "https" { if shasumsURL.Scheme != "http" && shasumsURL.Scheme != "https" {
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS URL: must use http or https scheme") return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS URL: must use http or https scheme")
} }
document, err := c.getFile(shasumsURL) document, err := c.getFile(ctx, shasumsURL)
if err != nil { if err != nil {
return PackageMeta{}, c.errQueryFailed( return PackageMeta{}, c.errQueryFailed(
provider, provider,
@@ -343,7 +381,7 @@ func (c *registryClient) PackageMeta(ctx context.Context, provider addrs.Provide
if signatureURL.Scheme != "http" && signatureURL.Scheme != "https" { if signatureURL.Scheme != "http" && signatureURL.Scheme != "https" {
return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS signature URL: must use http or https scheme") return PackageMeta{}, fmt.Errorf("registry response includes invalid SHASUMS signature URL: must use http or https scheme")
} }
signature, err := c.getFile(signatureURL) signature, err := c.getFile(ctx, signatureURL)
if err != nil { if err != nil {
return PackageMeta{}, c.errQueryFailed( return PackageMeta{}, c.errQueryFailed(
provider, provider,
@@ -434,8 +472,14 @@ func (c *registryClient) errUnauthorized(hostname svchost.Hostname) error {
} }
} }
func (c *registryClient) getFile(url *url.URL) ([]byte, error) { func (c *registryClient) getFile(ctx context.Context, url *url.URL) ([]byte, error) {
resp, err := c.httpClient.Get(url.String()) req, err := retryablehttp.NewRequest("GET", url.String(), nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -15,11 +15,15 @@ import (
"github.com/apparentlymart/go-versions/versions" "github.com/apparentlymart/go-versions/versions"
"github.com/apparentlymart/go-versions/versions/constraints" "github.com/apparentlymart/go-versions/versions/constraints"
otelAttr "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/addrs" "github.com/opentofu/opentofu/internal/addrs"
copydir "github.com/opentofu/opentofu/internal/copy" copydir "github.com/opentofu/opentofu/internal/copy"
"github.com/opentofu/opentofu/internal/depsfile" "github.com/opentofu/opentofu/internal/depsfile"
"github.com/opentofu/opentofu/internal/getproviders" "github.com/opentofu/opentofu/internal/getproviders"
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
) )
// Installer is the main type in this package, representing a provider installer // Installer is the main type in this package, representing a provider installer
@@ -439,20 +443,31 @@ func (i *Installer) ensureProviderVersionsInstall(
authResults := map[addrs.Provider]*getproviders.PackageAuthenticationResult{} // record auth results for all successfully fetched providers authResults := map[addrs.Provider]*getproviders.PackageAuthenticationResult{} // record auth results for all successfully fetched providers
for provider, version := range need { for provider, version := range need {
if err := ctx.Err(); err != nil { traceCtx, span := tracing.Tracer().Start(ctx,
"Install Provider",
trace.WithAttributes(
otelAttr.String(traceattrs.ProviderAddress, provider.String()),
otelAttr.String(traceattrs.ProviderVersion, version.String()),
),
)
if err := traceCtx.Err(); err != nil {
// If our context has been cancelled or reached a timeout then // If our context has been cancelled or reached a timeout then
// we'll abort early, because subsequent operations against // we'll abort early, because subsequent operations against
// that context will fail immediately anyway. // that context will fail immediately anyway.
tracing.SetSpanError(span, err)
span.End()
return nil, err return nil, err
} }
authResult, err := i.ensureProviderVersionInstall(ctx, locks, reqs, mode, provider, version, targetPlatform) authResult, err := i.ensureProviderVersionInstall(traceCtx, locks, reqs, mode, provider, version, targetPlatform)
if authResult != nil { if authResult != nil {
authResults[provider] = authResult authResults[provider] = authResult
} }
if err != nil { if err != nil {
errs[provider] = err errs[provider] = err
} }
span.End()
} }
return authResults, nil return authResults, nil
} }

128
internal/tracing/init.go Normal file
View File

@@ -0,0 +1,128 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tracing
import (
"context"
"fmt"
"log"
"os"
"github.com/go-logr/stdr"
"go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"github.com/opentofu/opentofu/version"
)
/*
BEWARE! This is not a committed external interface.
Everything about this is experimental and subject to change in future
releases. Do not depend on anything about the structure of this output.
This mechanism might be removed altogether if a different strategy seems
better based on experience with this experiment.
*/
// OTELExporterEnvVar is the env var that should be used to instruct opentofu which
// exporter to use
// If this environment variable is set to "otlp" when running OpenTofu CLI
// then we'll enable an experimental OTLP trace exporter.
const OTELExporterEnvVar = "OTEL_TRACES_EXPORTER"
// isTracingEnabled is true if OpenTelemetry is enabled.
var isTracingEnabled bool
// OpenTelemetryInit initializes the optional OpenTelemetry exporter.
//
// By default, we don't export telemetry information at all, since OpenTofu is
// a CLI tool, and so we don't assume we're running in an environment with
// a telemetry collector available.
//
// However, for those running OpenTofu in automation we allow setting
// the standard OpenTelemetry environment variable OTEL_TRACES_EXPORTER=otlp
// to enable an OTLP exporter, which is in turn configured by all the
// standard OTLP exporter environment variables:
//
// https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options
//
// We don't currently support any other telemetry export protocols, because
// OTLP has emerged as a de-facto standard and each other exporter we support
// means another relatively-heavy external dependency. OTLP happens to use
// protocol buffers and gRPC, which OpenTofu would depend on for other reasons
// anyway.
func OpenTelemetryInit() error {
isTracingEnabled = false
// We'll check the environment variable ourselves first, because the
// "autoexport" helper we're about to use is built under the assumption
// that exporting should always be enabled and so will expect to find
// an OTLP server on localhost if no environment variables are set at all.
if os.Getenv(OTELExporterEnvVar) != "otlp" {
return nil // By default, we just discard all telemetry calls
}
isTracingEnabled = true
log.Printf("[TRACE] OpenTelemetry: enabled")
otelResource, err := resource.New(context.Background(),
// Use built-in detectors to simplify the collation of the racing information
resource.WithOS(),
resource.WithHost(),
resource.WithProcess(),
resource.WithSchemaURL(semconv.SchemaURL),
resource.WithAttributes(),
// Add custom service attributes
resource.WithAttributes(
semconv.ServiceName("OpenTofu CLI"),
semconv.ServiceVersion(version.Version),
// We add in the telemetry SDK information so that we don't end up with
// duplicate schema urls that clash
semconv.TelemetrySDKName("opentelemetry"),
semconv.TelemetrySDKLanguageGo,
semconv.TelemetrySDKVersion(sdk.Version()),
),
)
if err != nil {
return fmt.Errorf("failed to create resource: %w", err)
}
exporter, err := autoexport.NewSpanExporter(context.Background())
if err != nil {
return err
}
// Set the global tracer provider, this allows us to use this global TracerProvider
// to create tracers around the project
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter,
sdktrace.WithBlocking(),
),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(otelResource),
)
otel.SetTracerProvider(provider)
prop := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
otel.SetTextMapPropagator(prop)
logger := stdr.New(log.New(os.Stdout, "", log.LstdFlags|log.Lshortfile))
otel.SetLogger(logger)
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
panic(fmt.Sprintf("OpenTelemetry error: %v", err))
}))
return nil
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package traceattrs
const (
// Common attributes names used across the codebase
ProviderAddress = "opentofu.provider.address"
ProviderVersion = "opentofu.provider.version"
)

121
internal/tracing/utils.go Normal file
View File

@@ -0,0 +1,121 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tracing
import (
"context"
"errors"
"log"
"runtime"
"strings"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/tfdiags"
)
func Tracer() trace.Tracer {
if !isTracingEnabled {
return otel.Tracer("")
}
pc, _, _, ok := runtime.Caller(1)
if !ok || runtime.FuncForPC(pc) == nil {
return otel.Tracer("")
}
// We use the import path of the caller function as the tracer name.
return otel.GetTracerProvider().Tracer(extractImportPath(runtime.FuncForPC(pc).Name()))
}
// SetSpanError sets the error or diagnostic information on the span.
// It accepts an error, a string, or a diagnostics object.
// It also sets the span status to Error and records the error or message.
func SetSpanError(span trace.Span, input any) {
if span == nil || input == nil {
return
}
switch v := input.(type) {
case error:
if v != nil {
span.SetStatus(codes.Error, v.Error())
span.RecordError(v)
}
case string:
if v != "" {
span.SetStatus(codes.Error, v)
span.RecordError(errors.New(v))
}
case tfdiags.Diagnostics: // Assuming Diagnostics is a custom type you have defined elsewhere
if v.HasErrors() { // Assuming IsEmpty() checks if the diagnostics object has content
span.SetStatus(codes.Error, v.Err().Error())
span.RecordError(v.Err())
}
default:
// Handle unsupported types gracefully
// TODO: Discuss if this should panic?
span.SetStatus(codes.Error, "ERROR: unsupported input type for SetSpanError.")
span.AddEvent("ERROR: unsupported input type for SetSpanError")
}
}
// ForceFlush ensures that all spans are exported to the collector before
// the application terminates. This is particularly important for CLI
// applications where the process exits immediately after the operation.
//
// This should be called before the application terminates to ensure
// all spans are exported properly.
func ForceFlush(timeout time.Duration) {
if !isTracingEnabled {
return
}
provider, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider)
if !ok {
log.Printf("[DEBUG] OpenTelemetry: tracer provider is not an SDK provider, can't force flush")
return
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
log.Printf("[DEBUG] OpenTelemetry: flushing spans")
if err := provider.ForceFlush(ctx); err != nil {
log.Printf("[WARN] OpenTelemetry: error flushing spans: %v", err)
}
}
// extractImportPath extracts the import path from a full function name.
// the function names returned by runtime.FuncForPC(pc).Name() can be in the following formats
//
// main.(*MyType).MyMethod
// github.com/you/pkg.(*SomeType).Method-fm
// github.com/you/pkg.functionName
func extractImportPath(fullName string) string {
lastSlash := strings.LastIndex(fullName, "/")
if lastSlash == -1 {
// When there is no slash, then use everything before the first dot
if dot := strings.Index(fullName, "."); dot != -1 {
return fullName[:dot]
}
log.Printf("[WARN] unable to extract import path from function name: %q. Tracing may be incomplete. This is a bug in OpenTofu, please report it.", fullName)
return "unknown"
}
dotAfterSlash := strings.Index(fullName[lastSlash:], ".")
if dotAfterSlash == -1 {
log.Printf("[WARN] unable to extract import path from function name: %q. Tracing may be incomplete. This is a bug in OpenTofu, please report it.", fullName)
return "unknown"
}
return fullName[:lastSlash+dotAfterSlash]
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tracing
import "testing"
func TestExtractImportPath(t *testing.T) {
tests := []struct {
fullName string
expected string
}{
{
fullName: "github.com/opentofu/opentofu/internal/getproviders.(*registryClient).Get",
expected: "github.com/opentofu/opentofu/internal/getproviders",
},
{
fullName: "github.com/opentofu/opentofu/pkg/module.Function",
expected: "github.com/opentofu/opentofu/pkg/module",
},
{
fullName: "main.main",
expected: "main",
},
{
fullName: "unknownFormat",
expected: "unknown",
},
}
for _, test := range tests {
got := extractImportPath(test.fullName)
if got != test.expected {
t.Errorf("extractImportPath(%q) = %q; want %q", test.fullName, got, test.expected)
}
}
}