mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
[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:
@@ -21,6 +21,7 @@ NEW FEATURES:
|
||||
- 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))
|
||||
- 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:
|
||||
|
||||
|
||||
@@ -15,13 +15,14 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/apparentlymart/go-shquot/shquot"
|
||||
"github.com/hashicorp/go-plugin"
|
||||
"github.com/hashicorp/terraform-svchost/disco"
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/command/cliconfig"
|
||||
"github.com/opentofu/opentofu/internal/command/format"
|
||||
@@ -29,8 +30,8 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/httpclient"
|
||||
"github.com/opentofu/opentofu/internal/logging"
|
||||
"github.com/opentofu/opentofu/internal/terminal"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
"github.com/opentofu/opentofu/version"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
backendInit "github.com/opentofu/opentofu/internal/backend/init"
|
||||
)
|
||||
@@ -69,25 +70,22 @@ func main() {
|
||||
func realMain() int {
|
||||
defer logging.PanicHandler()
|
||||
|
||||
var err error
|
||||
|
||||
err = openTelemetryInit()
|
||||
err := tracing.OpenTelemetryInit()
|
||||
if err != nil {
|
||||
// openTelemetryInit can only fail if OpenTofu was run with an
|
||||
// explicit environment variable to enable telemetry collection,
|
||||
// so in typical use we cannot get here.
|
||||
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
|
||||
}
|
||||
var ctx context.Context
|
||||
var otelSpan trace.Span
|
||||
{
|
||||
// At minimum we emit a span covering the entire command execution.
|
||||
_, displayArgs := shquot.POSIXShellSplit(os.Args)
|
||||
ctx, otelSpan = tracer.Start(context.Background(), fmt.Sprintf("tofu %s", displayArgs))
|
||||
defer otelSpan.End()
|
||||
}
|
||||
defer tracing.ForceFlush(5 * time.Second)
|
||||
ctx := context.Background()
|
||||
|
||||
// At minimum, we emit a span covering the entire command execution.
|
||||
ctx, span := tracing.Tracer().Start(ctx, "tofu")
|
||||
defer span.End()
|
||||
|
||||
tmpLogPath := os.Getenv(envTmpLogPath)
|
||||
if tmpLogPath != "" {
|
||||
@@ -102,9 +100,7 @@ func realMain() int {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf(
|
||||
"[INFO] OpenTofu version: %s %s",
|
||||
Version, VersionPrerelease)
|
||||
log.Printf("[INFO] OpenTofu version: %s %s", Version, VersionPrerelease)
|
||||
for _, depMod := range version.InterestingDependencies() {
|
||||
log.Printf("[DEBUG] using %s %s", depMod.Path, depMod.Version)
|
||||
}
|
||||
@@ -117,6 +113,7 @@ func realMain() int {
|
||||
streams, err := terminal.Init()
|
||||
if err != nil {
|
||||
Ui.Error(fmt.Sprintf("Failed to configure the terminal: %s", err))
|
||||
|
||||
return 1
|
||||
}
|
||||
if streams.Stdout.IsTerminal() {
|
||||
@@ -140,7 +137,7 @@ func realMain() int {
|
||||
// path in the TERRAFORM_CONFIG_FILE environment variable (though probably
|
||||
// ill-advised) will be resolved relative to the true working directory,
|
||||
// not the overridden one.
|
||||
config, diags := cliconfig.LoadConfig()
|
||||
config, diags := cliconfig.LoadConfig(ctx)
|
||||
|
||||
if len(diags) > 0 {
|
||||
// Since we haven't instantiated a command.Meta yet, we need to do
|
||||
|
||||
@@ -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
14
go.mod
@@ -34,9 +34,10 @@ require (
|
||||
github.com/cli/browser v1.3.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
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-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/googleapis/gax-go/v2 v2.12.0
|
||||
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-yaml v1.1.0
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.0.0-20230703072336-9a582bd098a2
|
||||
go.opentelemetry.io/otel v1.34.0
|
||||
go.opentelemetry.io/otel/sdk v1.33.0
|
||||
go.opentelemetry.io/otel/trace v1.34.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.uber.org/mock v0.4.0
|
||||
golang.org/x/crypto v0.35.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
@@ -180,7 +182,6 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.5.4 // 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/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/errors v0.20.2 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.3 // 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/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/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/otlptracegrpc 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
|
||||
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
|
||||
20
go.sum
20
go.sum
@@ -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.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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
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/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28=
|
||||
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/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/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
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/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
|
||||
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/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/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
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 v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package cliconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@@ -23,7 +24,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl"
|
||||
|
||||
svchost "github.com/hashicorp/terraform-svchost"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
@@ -116,7 +116,7 @@ func DataDirs() ([]string, error) {
|
||||
// LoadConfig reads the CLI configuration from the various filesystem locations
|
||||
// and from the environment, returning a merged configuration along with any
|
||||
// diagnostics (errors and warnings) encountered along the way.
|
||||
func LoadConfig() (*Config, tfdiags.Diagnostics) {
|
||||
func LoadConfig(_ context.Context) (*Config, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
configVal := BuiltinConfig // copy
|
||||
config := &configVal
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package cliconfig
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"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)
|
||||
|
||||
c, errs := LoadConfig()
|
||||
c, errs := LoadConfig(context.Background())
|
||||
if errs.HasErrors() || c.Validate().HasErrors() {
|
||||
t.Fatalf("err: %s", errs)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@ import (
|
||||
svchost "github.com/hashicorp/terraform-svchost"
|
||||
"github.com/posener/complete"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
otelAttr "go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
@@ -36,6 +35,7 @@ import (
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/opentofu/opentofu/internal/tofu"
|
||||
"github.com/opentofu/opentofu/internal/tofumigrate"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
tfversion "github.com/opentofu/opentofu/version"
|
||||
)
|
||||
|
||||
@@ -46,6 +46,11 @@ type InitCommand struct {
|
||||
}
|
||||
|
||||
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 flagBackend, flagCloud, flagGet, flagUpgrade bool
|
||||
var flagPluginPath FlagStringSlice
|
||||
@@ -129,7 +134,7 @@ func (c *InitCommand) Run(args []string) int {
|
||||
}
|
||||
|
||||
// Initialization can be aborted by interruption signals
|
||||
ctx, done := c.InterruptibleContext(c.CommandContext())
|
||||
ctx, done := c.InterruptibleContext(ctx)
|
||||
defer done()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes(
|
||||
attribute.String("module_source", src),
|
||||
ctx, span := tracing.Tracer().Start(ctx, "From module", trace.WithAttributes(
|
||||
otelAttr.String("opentofu.module_source", src),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks)
|
||||
diags = diags.Append(initDirFromModuleDiags)
|
||||
if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() {
|
||||
c.showDiagnostics(diags)
|
||||
span.SetStatus(codes.Error, "module installation failed")
|
||||
tracing.SetSpanError(span, initDirFromModuleDiags)
|
||||
span.End()
|
||||
return 1
|
||||
}
|
||||
span.End()
|
||||
|
||||
c.Ui.Output("")
|
||||
}
|
||||
@@ -402,8 +407,8 @@ func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, ear
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
ctx, span := tracer.Start(ctx, "install modules", trace.WithAttributes(
|
||||
attribute.Bool("upgrade", upgrade),
|
||||
ctx, span := tracing.Tracer().Start(ctx, "Get modules", trace.WithAttributes(
|
||||
otelAttr.Bool("opentofu.modules.upgrade", upgrade),
|
||||
))
|
||||
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) {
|
||||
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
|
||||
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) {
|
||||
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
|
||||
defer span.End()
|
||||
|
||||
@@ -555,7 +560,7 @@ the backend configuration is present and valid.
|
||||
// Load the complete module tree, and fetch any missing providers.
|
||||
// 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) {
|
||||
ctx, span := tracer.Start(ctx, "install providers")
|
||||
ctx, span := tracing.Tracer().Start(ctx, "Get Providers")
|
||||
defer span.End()
|
||||
|
||||
// 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.`))
|
||||
}
|
||||
|
||||
moreDiags = c.replaceLockedDependencies(newLocks)
|
||||
moreDiags = c.replaceLockedDependencies(ctx, newLocks)
|
||||
diags = diags.Append(moreDiags)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"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/configs"
|
||||
@@ -275,9 +273,6 @@ func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) {
|
||||
// this package has a reasonable implementation for displaying notifications
|
||||
// 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) {
|
||||
ctx, span := tracer.Start(ctx, "install modules")
|
||||
defer span.End()
|
||||
|
||||
rootDir = m.normalizePath(rootDir)
|
||||
|
||||
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
|
||||
// via a provided cli.Ui.
|
||||
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()
|
||||
if err != nil {
|
||||
diags = diags.Append(err)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
@@ -64,8 +65,8 @@ func (m *Meta) lockedDependencies() (*depsfile.Locks, tfdiags.Diagnostics) {
|
||||
// replaceLockedDependencies creates or overwrites the lock file in the
|
||||
// current working directory to contain the information recorded in the given
|
||||
// locks object.
|
||||
func (m *Meta) replaceLockedDependencies(new *depsfile.Locks) tfdiags.Diagnostics {
|
||||
return depsfile.SaveLocksToFile(new, dependencyLockFilename)
|
||||
func (m *Meta) replaceLockedDependencies(ctx context.Context, new *depsfile.Locks) tfdiags.Diagnostics {
|
||||
return depsfile.SaveLocksToFile(ctx, new, dependencyLockFilename)
|
||||
}
|
||||
|
||||
// annotateDependencyLocksWithOverrides modifies the given Locks object in-place
|
||||
|
||||
@@ -331,7 +331,7 @@ func (c *ProvidersLockCommand) Run(args []string) int {
|
||||
newLocks.SetProvider(provider, version, constraints, hashes)
|
||||
}
|
||||
|
||||
moreDiags = c.replaceLockedDependencies(newLocks)
|
||||
moreDiags = c.replaceLockedDependencies(ctx, newLocks)
|
||||
diags = diags.Append(moreDiags)
|
||||
|
||||
c.showDiagnostics(diags)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -51,7 +52,7 @@ func TestVersion(t *testing.T) {
|
||||
VersionPrerelease: "foo",
|
||||
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)
|
||||
}
|
||||
if code := c.Run([]string{}); code != 0 {
|
||||
@@ -149,7 +150,7 @@ func TestVersion_json(t *testing.T) {
|
||||
VersionPrerelease: "foo",
|
||||
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)
|
||||
}
|
||||
if code := c.Run([]string{"-json"}); code != 0 {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package depsfile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
@@ -15,11 +16,14 @@ import (
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/hcl/v2/hclwrite"
|
||||
"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/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/replacefile"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
"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
|
||||
// temporary files may be temporarily created in the same directory as the
|
||||
// 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)
|
||||
if diags.HasErrors() {
|
||||
tracing.SetSpanError(span, diags)
|
||||
return diags
|
||||
}
|
||||
|
||||
span.AddEvent("Serialized lockfile")
|
||||
span.SetAttributes(semconv.FileSize(len(src)))
|
||||
|
||||
err := replacefile.AtomicWriteFile(filename, src, 0644)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
@@ -103,6 +114,7 @@ func SaveLocksToFile(locks *Locks, filename string) tfdiags.Diagnostics {
|
||||
"Failed to update dependency lock file",
|
||||
fmt.Sprintf("Error while writing new dependency lock information to %s: %s.", filename, err),
|
||||
))
|
||||
tracing.SetSpanError(span, diags)
|
||||
return diags
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ package depsfile
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/getproviders"
|
||||
"github.com/opentofu/opentofu/internal/tfdiags"
|
||||
@@ -227,7 +229,7 @@ func TestSaveLocksToFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
filename := filepath.Join(dir, LockFilePath)
|
||||
diags := SaveLocksToFile(locks, filename)
|
||||
diags := SaveLocksToFile(context.Background(), locks, filename)
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
|
||||
}
|
||||
|
||||
@@ -15,8 +15,13 @@ import (
|
||||
|
||||
"github.com/hashicorp/go-getter"
|
||||
"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/logging"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
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
|
||||
// a zip file. We'll fetch that into a temporary file here and then
|
||||
// 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.HTTPClient = httpclient.New()
|
||||
retryableClient.HTTPClient.Transport = otelhttp.NewTransport(retryableClient.HTTPClient.Transport)
|
||||
retryableClient.RetryMax = maxHTTPPackageRetryCount
|
||||
retryableClient.RequestLogHook = func(logger retryablehttp.Logger, _ *http.Request, i int) {
|
||||
if i > 0 {
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
@@ -31,7 +34,10 @@ var _ PackageLocation = PackageLocalArchive("")
|
||||
|
||||
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
|
||||
if meta.Authentication != nil {
|
||||
var err error
|
||||
@@ -42,19 +48,24 @@ func (p PackageLocalArchive) InstallProviderPackage(_ context.Context, meta Pack
|
||||
|
||||
if len(allowedHashes) > 0 {
|
||||
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",
|
||||
meta.Provider, meta.Version, meta.Location, err,
|
||||
)
|
||||
tracing.SetSpanError(span, err)
|
||||
return authResult, err
|
||||
} 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",
|
||||
meta.Provider, meta.Version,
|
||||
)
|
||||
tracing.SetSpanError(span, err)
|
||||
return authResult, err
|
||||
}
|
||||
}
|
||||
|
||||
filename := meta.Location.String()
|
||||
span.SetAttributes(semconv.FilePath(filename))
|
||||
|
||||
// NOTE: We're not checking whether there's already a directory at
|
||||
// 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
|
||||
err := unzip.Decompress(targetDir, filename, true, 0000)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return authResult, err
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,16 @@ import (
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
svchost "github.com/hashicorp/terraform-svchost"
|
||||
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/httpclient"
|
||||
"github.com/opentofu/opentofu/internal/logging"
|
||||
"github.com/opentofu/opentofu/internal/tracing"
|
||||
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
|
||||
"github.com/opentofu/opentofu/version"
|
||||
)
|
||||
|
||||
@@ -82,6 +88,8 @@ func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registr
|
||||
retryableClient.RequestLogHook = requestLogHook
|
||||
retryableClient.ErrorHandler = maxRetryErrorHandler
|
||||
|
||||
retryableClient.HTTPClient.Transport = otelhttp.NewTransport(retryableClient.HTTPClient.Transport)
|
||||
|
||||
retryableClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
|
||||
|
||||
return ®istryClient{
|
||||
@@ -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
|
||||
// ErrQueryFailed for any other protocol or operational problem.
|
||||
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"))
|
||||
if err != nil {
|
||||
// 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
|
||||
}
|
||||
endpointURL := c.baseURL.ResolveReference(endpointPath)
|
||||
span.SetAttributes(semconv.URLFull(endpointURL.String()))
|
||||
req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -115,7 +131,9 @@ func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provid
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
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()
|
||||
|
||||
@@ -123,13 +141,19 @@ func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provid
|
||||
case http.StatusOK:
|
||||
// Great!
|
||||
case http.StatusNotFound:
|
||||
return nil, nil, ErrRegistryProviderNotKnown{
|
||||
err := ErrRegistryProviderNotKnown{
|
||||
Provider: addr,
|
||||
}
|
||||
tracing.SetSpanError(span, err)
|
||||
return nil, nil, err
|
||||
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:
|
||||
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
|
||||
@@ -146,7 +170,9 @@ func (c *registryClient) ProviderVersions(ctx context.Context, addr addrs.Provid
|
||||
|
||||
dec := json.NewDecoder(resp.Body)
|
||||
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 {
|
||||
@@ -181,12 +207,23 @@ func (c *registryClient) PackageMeta(ctx context.Context, provider addrs.Provide
|
||||
target.OS,
|
||||
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 {
|
||||
// Should never happen because we're constructing this from
|
||||
// already-validated components.
|
||||
return PackageMeta{}, err
|
||||
}
|
||||
endpointURL := c.baseURL.ResolveReference(endpointPath)
|
||||
span.SetAttributes(
|
||||
semconv.URLFull(endpointURL.String()),
|
||||
)
|
||||
|
||||
req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil)
|
||||
if err != nil {
|
||||
@@ -197,6 +234,7 @@ func (c *registryClient) PackageMeta(ctx context.Context, provider addrs.Provide
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
return PackageMeta{}, c.errQueryFailed(provider, err)
|
||||
}
|
||||
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" {
|
||||
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 {
|
||||
return PackageMeta{}, c.errQueryFailed(
|
||||
provider,
|
||||
@@ -343,7 +381,7 @@ func (c *registryClient) PackageMeta(ctx context.Context, provider addrs.Provide
|
||||
if signatureURL.Scheme != "http" && signatureURL.Scheme != "https" {
|
||||
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 {
|
||||
return PackageMeta{}, c.errQueryFailed(
|
||||
provider,
|
||||
@@ -434,8 +472,14 @@ func (c *registryClient) errUnauthorized(hostname svchost.Hostname) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *registryClient) getFile(url *url.URL) ([]byte, error) {
|
||||
resp, err := c.httpClient.Get(url.String())
|
||||
func (c *registryClient) getFile(ctx context.Context, url *url.URL) ([]byte, error) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@ import (
|
||||
|
||||
"github.com/apparentlymart/go-versions/versions"
|
||||
"github.com/apparentlymart/go-versions/versions/constraints"
|
||||
otelAttr "go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
copydir "github.com/opentofu/opentofu/internal/copy"
|
||||
"github.com/opentofu/opentofu/internal/depsfile"
|
||||
"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
|
||||
@@ -439,20 +443,31 @@ func (i *Installer) ensureProviderVersionsInstall(
|
||||
authResults := map[addrs.Provider]*getproviders.PackageAuthenticationResult{} // record auth results for all successfully fetched providers
|
||||
|
||||
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
|
||||
// we'll abort early, because subsequent operations against
|
||||
// that context will fail immediately anyway.
|
||||
tracing.SetSpanError(span, err)
|
||||
span.End()
|
||||
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 {
|
||||
authResults[provider] = authResult
|
||||
}
|
||||
if err != nil {
|
||||
errs[provider] = err
|
||||
}
|
||||
span.End()
|
||||
}
|
||||
return authResults, nil
|
||||
}
|
||||
|
||||
128
internal/tracing/init.go
Normal file
128
internal/tracing/init.go
Normal 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
|
||||
}
|
||||
13
internal/tracing/traceattrs/traceattrs.go
Normal file
13
internal/tracing/traceattrs/traceattrs.go
Normal 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
121
internal/tracing/utils.go
Normal 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]
|
||||
}
|
||||
39
internal/tracing/utils_test.go
Normal file
39
internal/tracing/utils_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user