[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))
- 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:

View File

@@ -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

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/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
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.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=

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

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
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 {

View File

@@ -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
}

View File

@@ -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())
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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 &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
// 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
}

View File

@@ -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
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)
}
}
}